diff --git a/Cargo.lock b/Cargo.lock index d5626037..094e2f35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1520,6 +1520,7 @@ dependencies = [ "quote", "radium", "regex", + "rpl_config", "rpl_interface", "rpl_meta", "rustc_tools_util", @@ -1538,6 +1539,17 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rpl_config" +version = "0.1.0" +dependencies = [ + "cargo_metadata 0.15.4", + "serde", + "serde_json", + "thiserror", + "toml", +] + [[package]] name = "rpl_constraints" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index cb396bc3..95db2a69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ rpl_mir_graph = { path = "./crates/rpl_mir_graph" } rpl_mir_transform = { path = "./crates/rpl_mir_transform" } rpl_utils = { path = "./crates/rpl_utils" } rpl_meta = { path = "./crates/rpl_meta" } +rpl_config = { path = "./crates/rpl_config" } rpl_constraints = { path = "./crates/rpl_constraints" } rpl_resolve = { path = "./crates/rpl_resolve" } rustc_tools_util = "0.3.0" @@ -90,6 +91,7 @@ path = "src/driver.rs" [dependencies] rpl_interface.workspace = true rpl_meta.workspace = true +rpl_config.workspace = true rustc_tools_util.workspace = true color-print = "0.3.4" anstream = "0.6.0" diff --git a/crates/rpl_config/Cargo.toml b/crates/rpl_config/Cargo.toml new file mode 100644 index 00000000..ec0d586d --- /dev/null +++ b/crates/rpl_config/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rpl_config" +version.workspace = true +description.workspace = true +repository.workspace = true +license.workspace = true +keywords.workspace = true +edition.workspace = true + +[dependencies] +cargo_metadata = "0.15.4" +serde.workspace = true +serde_json.workspace = true +toml.workspace = true +thiserror.workspace = true diff --git a/crates/rpl_config/src/lib.rs b/crates/rpl_config/src/lib.rs new file mode 100644 index 00000000..0b889a65 --- /dev/null +++ b/crates/rpl_config/src/lib.rs @@ -0,0 +1,74 @@ +use std::path::{Path, PathBuf}; + +use serde::Deserialize; + +mod patterns; +mod run; +mod util; + +#[derive(Debug, thiserror::Error)] +pub enum ConfigError { + #[error("failed to read {path}: {source}")] + Io { path: PathBuf, source: Box }, + #[error("failed to parse {path}: {source}")] + Toml { + path: PathBuf, + source: Box, + }, + #[error("cargo metadata failed: {0}")] + CargoMetadata(#[from] Box), + #[error("rpl.toml not found at {path}")] + ConfigNotFound { path: PathBuf }, + #[error("rpl.toml defines no pattern groups")] + NoGroups, + #[error("duplicate pattern group name `{name}`")] + DuplicateGroup { name: String }, + #[error("unknown pattern group `{name}`")] + UnknownGroup { name: String }, + #[error("invalid remote group reference `{spec}`; expected `crate::group`")] + InvalidRemoteGroup { spec: String }, + #[error("crate `{crate_name}` not found in cargo metadata")] + CrateNotFound { crate_name: String }, + #[error("multiple crates named `{crate_name}` found; disambiguation not supported")] + AmbiguousCrate { crate_name: String }, + #[error("crate `{crate_name}` does not publish RPL metadata")] + MissingRplMetadata { crate_name: String }, + #[error("crate `{crate_name}` has invalid RPL metadata: {error}")] + InvalidRplMetadata { crate_name: String, error: String }, + #[error("crate `{crate_name}` does not define RPL group `{group}`")] + MissingRemoteGroup { crate_name: String, group: String }, + #[error("pattern group name cannot be empty")] + EmptyGroupName, + #[error("pattern path is not valid unicode: {path}")] + NonUnicodePath { path: PathBuf }, + #[error("pattern paths cannot be joined for environment variable: {source}")] + InvalidPatternPathList { source: Box }, +} + +#[derive(Debug, Deserialize)] +struct RplConfig { + run: Option, + patterns: Option, +} + +#[derive(Debug)] +pub struct Config { + pub patterns_env: Option, + pub inline_mir: Option, +} + +pub fn load_config(manifest_path: Option<&Path>, selected_groups: &[String]) -> Result { + let base_dir = util::resolve_base_dir(manifest_path)?; + let config_path = base_dir.join("rpl.toml"); + let config = if config_path.exists() { + Some(util::read_config(&config_path)?) + } else { + None + }; + let inline_mir = run::load_inline_mir(config.as_ref()); + let patterns_env = patterns::load_patterns_env(manifest_path, selected_groups, config.as_ref())?; + Ok(Config { + patterns_env, + inline_mir, + }) +} diff --git a/crates/rpl_config/src/patterns.rs b/crates/rpl_config/src/patterns.rs new file mode 100644 index 00000000..85371a06 --- /dev/null +++ b/crates/rpl_config/src/patterns.rs @@ -0,0 +1,286 @@ +use std::collections::{HashMap, HashSet}; +use std::path::{Path, PathBuf}; + +use cargo_metadata::{Metadata, MetadataCommand, Package}; +use serde::Deserialize; + +use crate::util::resolve_base_dir; +use crate::{ConfigError, RplConfig}; + +pub(crate) struct ResolvedPatterns { + pub paths: Vec, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct PatternsConfig { + pub local: Option>, + pub remote: Option>, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct LocalGroup { + pub name: String, + pub path: Vec, +} + +#[derive(Debug, Deserialize)] +pub(crate) struct RemoteGroup { + pub name: String, + pub groups: Vec, +} + +#[derive(Debug, Deserialize)] +struct PackageMetadata { + rpl: Option, +} + +#[derive(Debug, Deserialize)] +struct PublishedRpl { + path: Option>, + groups: Option>>, +} + +pub(crate) fn resolve_patterns( + manifest_path: Option<&Path>, + selected_groups: &[String], + config: Option<&RplConfig>, +) -> Result, ConfigError> { + if selected_groups.iter().any(|group| group.is_empty()) { + return Err(ConfigError::EmptyGroupName); + } + let base_dir = resolve_base_dir(manifest_path)?; + let config_path = base_dir.join("rpl.toml"); + + if config.is_none() { + if selected_groups.is_empty() { + return Ok(None); + } + if selected_groups.iter().all(|name| is_remote_spec(name)) { + return Ok(Some(resolve_remote_selection(manifest_path, selected_groups)?)); + } + return Err(ConfigError::ConfigNotFound { path: config_path }); + } + + let config = config.unwrap(); + let patterns = match config.patterns.as_ref() { + Some(patterns) => patterns, + None => { + if selected_groups.iter().all(|name| is_remote_spec(name)) { + return Ok(Some(resolve_remote_selection(manifest_path, selected_groups)?)); + } + return Err(ConfigError::NoGroups); + }, + }; + + let mut local_groups: HashMap> = HashMap::new(); + let mut remote_groups: HashMap> = HashMap::new(); + let mut order = Vec::new(); + + if let Some(local) = patterns.local.as_ref() { + for entry in local { + if local_groups.contains_key(&entry.name) || remote_groups.contains_key(&entry.name) { + return Err(ConfigError::DuplicateGroup { + name: entry.name.clone(), + }); + } + local_groups.insert(entry.name.clone(), entry.path.clone()); + order.push(entry.name.clone()); + } + } + + if let Some(remote) = patterns.remote.as_ref() { + for entry in remote { + if local_groups.contains_key(&entry.name) || remote_groups.contains_key(&entry.name) { + return Err(ConfigError::DuplicateGroup { + name: entry.name.clone(), + }); + } + remote_groups.insert(entry.name.clone(), entry.groups.clone()); + order.push(entry.name.clone()); + } + } + + if local_groups.is_empty() && remote_groups.is_empty() { + return Err(ConfigError::NoGroups); + } + + let selected = if selected_groups.is_empty() { + order + } else { + selected_groups.to_vec() + }; + + let mut seen = HashSet::new(); + let mut paths = Vec::new(); + let mut metadata: Option = None; + + for name in &selected { + let group_paths = if let Some(entries) = local_groups.get(name) { + resolve_local_group(&base_dir, entries) + } else if let Some(entries) = remote_groups.get(name) { + if metadata.is_none() { + metadata = Some(load_metadata(manifest_path)?); + } + resolve_remote_groups(metadata.as_ref().unwrap(), entries)? + } else if is_remote_spec(name) { + if metadata.is_none() { + metadata = Some(load_metadata(manifest_path)?); + } + resolve_remote_groups(metadata.as_ref().unwrap(), std::slice::from_ref(name))? + } else { + return Err(ConfigError::UnknownGroup { name: name.clone() }); + }; + + for path in group_paths { + if seen.insert(path.clone()) { + paths.push(path); + } + } + } + + Ok(Some(ResolvedPatterns { paths })) +} + +pub(crate) fn resolve_patterns_env( + manifest_path: Option<&Path>, + selected_groups: &[String], + config: Option<&RplConfig>, +) -> Result, ConfigError> { + let resolved = match resolve_patterns(manifest_path, selected_groups, config)? { + Some(resolved) => resolved, + None => return Ok(None), + }; + + let mut entries = Vec::with_capacity(resolved.paths.len()); + for path in resolved.paths { + if path.to_str().is_none() { + return Err(ConfigError::NonUnicodePath { path }); + } + entries.push(path); + } + + let joined = std::env::join_paths(entries.iter()).map_err(|source| ConfigError::InvalidPatternPathList { + source: Box::new(source), + })?; + Ok(Some(joined.to_string_lossy().into_owned())) +} + +pub(crate) fn load_patterns_env( + manifest_path: Option<&Path>, + selected_groups: &[String], + config: Option<&RplConfig>, +) -> Result, ConfigError> { + let has_selection = !selected_groups.is_empty(); + if std::env::var("RPL_PATS").is_ok() && !has_selection { + return Ok(None); + } + resolve_patterns_env(manifest_path, selected_groups, config) +} + +fn resolve_local_group(base_dir: &Path, entries: &[String]) -> Vec { + entries.iter().map(|entry| resolve_relative(base_dir, entry)).collect() +} + +fn resolve_remote_groups(metadata: &Metadata, specs: &[String]) -> Result, ConfigError> { + let mut resolved = Vec::new(); + for spec in specs { + let (crate_name, group) = parse_remote_spec(spec)?; + let package = find_package(metadata, &crate_name)?; + let published = package_rpl_metadata(package, &crate_name)?; + + let group_entries = published + .groups + .as_ref() + .and_then(|groups| groups.get(&group)) + .ok_or_else(|| ConfigError::MissingRemoteGroup { + crate_name: crate_name.clone(), + group: group.clone(), + })?; + + let base_paths = published.path.as_ref().filter(|paths| !paths.is_empty()); + let default_base = vec![".".to_string()]; + let base_paths = base_paths.unwrap_or(&default_base); + + let manifest_path = PathBuf::from(package.manifest_path.as_str()); + let crate_root = manifest_path.parent().unwrap_or(Path::new(".")); + + for base in base_paths { + let base_dir = resolve_relative(crate_root, base); + for entry in group_entries { + resolved.push(resolve_relative(&base_dir, entry)); + } + } + } + Ok(resolved) +} + +fn parse_remote_spec(spec: &str) -> Result<(String, String), ConfigError> { + let (crate_name, group) = spec + .split_once("::") + .ok_or_else(|| ConfigError::InvalidRemoteGroup { spec: spec.to_string() })?; + Ok((crate_name.to_string(), group.to_string())) +} + +fn find_package<'a>(metadata: &'a Metadata, name: &str) -> Result<&'a Package, ConfigError> { + let matches: Vec<&Package> = metadata.packages.iter().filter(|pkg| pkg.name == name).collect(); + match matches.as_slice() { + [] => Err(ConfigError::CrateNotFound { + crate_name: name.to_string(), + }), + [pkg] => Ok(*pkg), + _ => Err(ConfigError::AmbiguousCrate { + crate_name: name.to_string(), + }), + } +} + +fn package_rpl_metadata(package: &Package, crate_name: &str) -> Result { + let metadata = serde_json::from_value::(package.metadata.clone()).map_err(|err| { + ConfigError::InvalidRplMetadata { + crate_name: crate_name.to_string(), + error: err.to_string(), + } + })?; + metadata.rpl.ok_or_else(|| ConfigError::MissingRplMetadata { + crate_name: crate_name.to_string(), + }) +} + +fn load_metadata(manifest_path: Option<&Path>) -> Result { + let mut cmd = MetadataCommand::new(); + if let Some(manifest_path) = manifest_path { + cmd.manifest_path(manifest_path); + } + cmd.exec().map_err(|err| ConfigError::CargoMetadata(Box::new(err))) +} + +fn resolve_relative(base: &Path, entry: &str) -> PathBuf { + let path = Path::new(entry); + if path.is_absolute() { + path.to_path_buf() + } else { + base.join(path) + } +} + +fn is_remote_spec(name: &str) -> bool { + name.contains("::") +} + +fn resolve_remote_selection( + manifest_path: Option<&Path>, + selected_groups: &[String], +) -> Result { + let metadata = load_metadata(manifest_path)?; + let mut seen = HashSet::new(); + let mut paths = Vec::new(); + for spec in selected_groups { + let group_paths = resolve_remote_groups(&metadata, std::slice::from_ref(spec))?; + for path in group_paths { + if seen.insert(path.clone()) { + paths.push(path); + } + } + } + Ok(ResolvedPatterns { paths }) +} diff --git a/crates/rpl_config/src/run.rs b/crates/rpl_config/src/run.rs new file mode 100644 index 00000000..195c62ef --- /dev/null +++ b/crates/rpl_config/src/run.rs @@ -0,0 +1,13 @@ +use serde::Deserialize; + +use crate::RplConfig; + +#[derive(Debug, Deserialize)] +pub(crate) struct RunConfig { + #[serde(alias = "inline-mir")] + pub inline_mir: Option, +} + +pub(crate) fn load_inline_mir(config: Option<&RplConfig>) -> Option { + config.and_then(|config| config.run.as_ref().and_then(|run| run.inline_mir)) +} diff --git a/crates/rpl_config/src/util.rs b/crates/rpl_config/src/util.rs new file mode 100644 index 00000000..03f47068 --- /dev/null +++ b/crates/rpl_config/src/util.rs @@ -0,0 +1,38 @@ +use std::path::{Path, PathBuf}; +use std::{env, fs}; + +use crate::{ConfigError, RplConfig}; + +pub(crate) fn resolve_base_dir(manifest_path: Option<&Path>) -> Result { + let base = if let Some(manifest_path) = manifest_path { + let manifest_path = if manifest_path.is_absolute() { + manifest_path.to_path_buf() + } else { + env::current_dir() + .map_err(|source| ConfigError::Io { + path: PathBuf::from("."), + source: Box::new(source), + })? + .join(manifest_path) + }; + manifest_path.parent().unwrap_or(Path::new(".")).to_path_buf() + } else { + env::current_dir().map_err(|source| ConfigError::Io { + path: PathBuf::from("."), + source: Box::new(source), + })? + }; + + Ok(base) +} + +pub(crate) fn read_config(path: &Path) -> Result { + let contents = fs::read_to_string(path).map_err(|source| ConfigError::Io { + path: path.to_path_buf(), + source: Box::new(source), + })?; + toml::from_str(&contents).map_err(|source| ConfigError::Toml { + path: path.to_path_buf(), + source: Box::new(source), + }) +} diff --git a/src/main.rs b/src/main.rs index 24d7d564..740b5d2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,22 +44,54 @@ pub fn main() { } } +#[derive(Debug)] struct RplCmd { cargo_subcommand: &'static str, args: Vec, rpl_args: Vec, + pattern_groups: Vec, + manifest_path: Option, } impl RplCmd { - fn new(mut old_args: I) -> Self + fn new(mut old_args: I) -> Result where I: Iterator, { let mut cargo_subcommand = "check"; - let mut args = vec![]; - let mut rpl_args: Vec = vec![]; + let mut args = Vec::new(); + let mut rpl_args = Vec::new(); + let mut pattern_groups = Vec::new(); + let mut manifest_path = None; + let mut after_dashdash = false; + + let iter = old_args.by_ref(); + while let Some(arg) = iter.next() { + if arg == "--" { + after_dashdash = true; + continue; + } + + if after_dashdash { + rpl_args.push(arg); + continue; + } + + if let Some(value) = arg.strip_prefix("--patterns=") { + if value.is_empty() { + return Err("`--patterns` requires a non-empty value".to_string()); + } + pattern_groups.push(value.to_string()); + continue; + } + if arg == "--patterns" { + match iter.next() { + Some(value) if !value.is_empty() => pattern_groups.push(value), + _ => return Err("`--patterns` requires a value".to_string()), + } + continue; + } - for arg in old_args.by_ref() { match arg.as_str() { "--fix" => { cargo_subcommand = "fix"; @@ -69,23 +101,39 @@ impl RplCmd { rpl_args.push("--no-deps".into()); continue; }, - "--" => break, _ => {}, } + if let Some(value) = arg.strip_prefix("--manifest-path=") { + manifest_path = Some(PathBuf::from(value)); + args.push(arg); + continue; + } + + if arg == "--manifest-path" { + if let Some(value) = iter.next() { + manifest_path = Some(PathBuf::from(&value)); + args.push(arg); + args.push(value); + } else { + args.push(arg); + } + continue; + } + args.push(arg); } - - rpl_args.append(&mut (old_args.collect())); if cargo_subcommand == "fix" && !rpl_args.iter().any(|arg| arg == "--no-deps") { rpl_args.push("--no-deps".into()); } - Self { + Ok(Self { cargo_subcommand, args, rpl_args, - } + pattern_groups, + manifest_path, + }) } fn path() -> PathBuf { @@ -120,9 +168,28 @@ fn process(old_args: I) -> Result<(), i32> where I: Iterator, { - let cmd = RplCmd::new(old_args); + let cmd = match RplCmd::new(old_args) { + Ok(cmd) => cmd, + Err(err) => { + eprintln!("{err}"); + return Err(1); + }, + }; + let config = match rpl_config::load_config(cmd.manifest_path.as_deref(), &cmd.pattern_groups) { + Ok(value) => value, + Err(err) => { + eprintln!("{err}"); + return Err(1); + }, + }; let mut cmd = cmd.into_std_cmd(); + if let Some(patterns_env) = config.patterns_env { + cmd.env("RPL_PATS", patterns_env); + } + if let Some(inline_mir) = config.inline_mir { + apply_inline_mir(&mut cmd, inline_mir); + } let exit_status = cmd .spawn() @@ -137,6 +204,22 @@ where } } +fn apply_inline_mir(cmd: &mut Command, inline_mir: bool) { + let flag = format!("-Zinline-mir={inline_mir}"); + match env::var("RUSTFLAGS") { + Ok(existing) if existing.contains("inline-mir") => {}, + Ok(existing) if existing.trim().is_empty() => { + cmd.env("RUSTFLAGS", flag); + }, + Ok(existing) => { + cmd.env("RUSTFLAGS", format!("{existing} {flag}")); + }, + Err(_) => { + cmd.env("RUSTFLAGS", flag); + }, + } +} + #[must_use] pub fn help_message() -> &'static str { color_print::cstr!( @@ -148,6 +231,7 @@ pub fn help_message() -> &'static str { Common options: --no-deps Run RPL only on the given crate, without linting the dependencies --fix Automatically apply lint suggestions. This flag implies --no-deps and --all-targets + --patterns <> Run RPL with selected pattern groups (repeatable) -h, --help Print this message -V, --version Print version info and exit --explain [LINT] Print the documentation for a given lint @@ -177,7 +261,7 @@ mod tests { #[test] fn fix() { let args = "cargo rpl --fix".split_whitespace().map(ToString::to_string); - let cmd = RplCmd::new(args); + let cmd = RplCmd::new(args).expect("parse args"); assert_eq!("fix", cmd.cargo_subcommand); assert!(!cmd.args.iter().any(|arg| arg.ends_with("unstable-options"))); } @@ -185,7 +269,7 @@ mod tests { #[test] fn fix_implies_no_deps() { let args = "cargo rpl --fix".split_whitespace().map(ToString::to_string); - let cmd = RplCmd::new(args); + let cmd = RplCmd::new(args).expect("parse args"); assert!(cmd.rpl_args.iter().any(|arg| arg == "--no-deps")); } @@ -194,14 +278,61 @@ mod tests { let args = "cargo rpl --fix -- --no-deps" .split_whitespace() .map(ToString::to_string); - let cmd = RplCmd::new(args); + let cmd = RplCmd::new(args).expect("parse args"); assert_eq!(cmd.rpl_args.iter().filter(|arg| *arg == "--no-deps").count(), 1); } #[test] fn check() { let args = "cargo rpl".split_whitespace().map(ToString::to_string); - let cmd = RplCmd::new(args); + let cmd = RplCmd::new(args).expect("parse args"); assert_eq!("check", cmd.cargo_subcommand); } + + #[test] + fn patterns_equals() { + let args = "cargo rpl --patterns=core".split_whitespace().map(ToString::to_string); + let cmd = RplCmd::new(args).expect("parse args"); + assert_eq!(cmd.pattern_groups, vec!["core".to_string()]); + } + + #[test] + fn patterns_space() { + let args = "cargo rpl --patterns core".split_whitespace().map(ToString::to_string); + let cmd = RplCmd::new(args).expect("parse args"); + assert_eq!(cmd.pattern_groups, vec!["core".to_string()]); + } + + #[test] + fn patterns_multiple() { + let args = "cargo rpl --patterns=core --patterns extra" + .split_whitespace() + .map(ToString::to_string); + let cmd = RplCmd::new(args).expect("parse args"); + assert_eq!(cmd.pattern_groups, vec!["core".to_string(), "extra".to_string()]); + } + + #[test] + fn patterns_missing_value() { + let args = "cargo rpl --patterns".split_whitespace().map(ToString::to_string); + let err = RplCmd::new(args).expect_err("missing value should error"); + assert!(err.contains("--patterns")); + } + + #[test] + fn patterns_empty_value() { + let args = "cargo rpl --patterns=".split_whitespace().map(ToString::to_string); + let err = RplCmd::new(args).expect_err("empty value should error"); + assert!(err.contains("--patterns")); + } + + #[test] + fn patterns_after_dashdash() { + let args = "cargo rpl -- --patterns=core" + .split_whitespace() + .map(ToString::to_string); + let cmd = RplCmd::new(args).expect("parse args"); + assert!(cmd.pattern_groups.is_empty()); + assert!(cmd.rpl_args.iter().any(|arg| arg == "--patterns=core")); + } }