use anyhow::Context;
use anyhow::Result;
use std::borrow::Borrow;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
use std::str::Split;
use thiserror::Error;
use crate::arg_parser::ConfigDiscovery;
use crate::arg_parser::FilePatternArgs;
use crate::configuration::ResolvedConfig;
use crate::environment::CanonicalizedPathBuf;
use crate::environment::Environment;
use crate::patterns::get_all_file_patterns;
use crate::patterns::process_cli_path_args;
use crate::patterns::process_config_patterns;
use crate::plugins::PluginNameResolutionMaps;
use crate::resolution::PluginWithConfig;
use crate::utils::GlobOptions;
use crate::utils::GlobOutput;
use crate::utils::GlobPattern;
use crate::utils::GlobPatterns;
use crate::utils::glob;
use crate::utils::is_negated_glob;
use crate::utils::read_file_shebang_line;
#[derive(Debug, Eq, PartialEq, Hash)]
pub struct PluginNames(String);
impl PluginNames {
const SEPARATOR: &'static str = "~~";
pub fn names(&self) -> Split<'_, &str> {
self.0.split(PluginNames::SEPARATOR)
}
}
impl Borrow<str> for PluginNames {
fn borrow(&self) -> &str {
&self.0
}
}
#[derive(Default)]
struct PluginNamesBuilder {
buffer: String,
}
impl PluginNamesBuilder {
fn build(&mut self, names: &[&str]) -> PluginNamesKey<'_> {
self.buffer.clear();
for (i, name) in names.iter().enumerate() {
if i > 0 {
self.buffer.push_str(PluginNames::SEPARATOR);
}
self.buffer.push_str(name);
}
PluginNamesKey(&self.buffer)
}
}
struct PluginNamesKey<'a>(&'a str);
impl PluginNamesKey<'_> {
fn as_str(&self) -> &str {
self.0
}
fn to_plugin_names(&self) -> PluginNames {
PluginNames(self.0.to_string())
}
}
#[derive(Debug, Error)]
#[error("No files found to format with the specified plugins at {}. You may want to try using `dprint output-file-paths` to see which files it's finding or run with `--allow-no-files`.", .base_path.display())]
pub struct NoFilesFoundError {
pub base_path: CanonicalizedPathBuf,
}
pub struct FilesPathsByPlugins(HashMap<PluginNames, Vec<PathBuf>>);
impl FilesPathsByPlugins {
pub fn ensure_not_empty(&self, base_path: &CanonicalizedPathBuf) -> Result<(), NoFilesFoundError> {
if self.is_empty() {
Err(NoFilesFoundError { base_path: base_path.clone() })
} else {
Ok(())
}
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn into_vec(self) -> Vec<(PluginNames, Vec<PathBuf>)> {
self.0.into_iter().collect()
}
pub fn all_file_paths(&self) -> impl Iterator<Item = &PathBuf> {
self.0.values().flatten()
}
}
pub fn get_file_paths_by_plugins(
plugin_name_maps: &PluginNameResolutionMaps,
file_paths: Vec<PathBuf>,
mut shebang_lines: HashMap<PathBuf, Vec<u8>>,
environment: &impl Environment,
) -> Result<FilesPathsByPlugins> {
let mut file_paths_by_plugin: HashMap<PluginNames, Vec<PathBuf>> = HashMap::new();
let mut plugin_names_builder = PluginNamesBuilder::default();
for file_path in file_paths.into_iter() {
let plugin_names = match shebang_lines.remove(&file_path) {
Some(shebang_line) => plugin_name_maps.get_plugin_names_from_file_path_and_bytes(&file_path, &shebang_line),
None => get_plugin_names_for_file_on_disk(plugin_name_maps, &file_path, environment),
};
if !plugin_names.is_empty() {
let key = plugin_names_builder.build(&plugin_names);
match file_paths_by_plugin.get_mut(key.as_str()) {
Some(file_paths) => file_paths.push(file_path),
None => {
file_paths_by_plugin.insert(key.to_plugin_names(), vec![file_path]);
}
}
}
}
Ok(FilesPathsByPlugins(file_paths_by_plugin))
}
pub fn get_plugin_names_for_file_on_disk<'a>(plugin_name_maps: &'a PluginNameResolutionMaps, file_path: &Path, environment: &impl Environment) -> Vec<&'a str> {
let path_plugin_names = plugin_name_maps.get_plugin_names_from_file_path(file_path);
if path_plugin_names.has_claiming_plugin() || !plugin_name_maps.may_match_shebang(file_path) {
return path_plugin_names.into_names();
}
match read_file_shebang_line(environment, file_path) {
Ok(Some(shebang_line)) => plugin_name_maps
.get_plugin_names_from_shebang(file_path, &shebang_line)
.unwrap_or_else(|| path_plugin_names.into_names()),
_ => path_plugin_names.into_names(),
}
}
pub async fn get_and_resolve_file_paths<'a>(
config: &ResolvedConfig,
args: &FilePatternArgs,
config_discovery: ConfigDiscovery,
include_scope: IncludeScope,
plugins: impl Iterator<Item = &'a PluginWithConfig>,
environment: &impl Environment,
) -> Result<GlobOutput> {
let cwd = environment.cwd();
let mut file_patterns = get_all_file_patterns(config, args, &cwd, environment);
if args.only_staged {
let staged_files = environment.get_staged_files().context("Failed running git staged.")?;
file_patterns.arg_includes = Some(process_cli_path_args(&staged_files, &cwd, environment));
} else if args.only_dirty {
let dirty_files = environment.get_dirty_files().context("Failed running git status.")?;
file_patterns.arg_includes = Some(process_cli_path_args(&dirty_files, &cwd, environment));
}
if file_patterns.config_includes.is_none() {
file_patterns.config_includes = Some(GlobPattern::new_vec(get_plugin_patterns(plugins), config.base_path.clone()));
}
get_and_resolve_file_patterns(config, file_patterns, args.no_gitignore, config_discovery, include_scope, environment).await
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IncludeScope {
AllowAncestors,
ConfigDir,
}
async fn get_and_resolve_file_patterns(
config: &ResolvedConfig,
file_patterns: GlobPatterns,
no_gitignore: bool,
config_discovery: ConfigDiscovery,
include_scope: IncludeScope,
environment: &impl Environment,
) -> Result<GlobOutput> {
let cwd = environment.cwd();
let is_cwd_in_base = cwd.starts_with(&config.base_path);
let is_in_sub_dir = cwd != config.base_path && is_cwd_in_base;
let pattern_base = match include_scope {
IncludeScope::AllowAncestors => get_includes_base_dir(&config.base_path, &file_patterns),
IncludeScope::ConfigDir => config.base_path.clone(),
};
let start_dir = if is_in_sub_dir { cwd } else { pattern_base.clone() };
let environment = environment.clone();
let current_config_path = config.source.maybe_local_path().map(|p| p.as_ref().to_path_buf());
dprint_core::async_runtime::spawn_blocking(move || {
glob(
&environment,
GlobOptions {
start_dir: start_dir.into_path_buf(),
file_patterns,
pattern_base,
config_discovery,
current_config_path,
no_gitignore,
},
)
})
.await
.unwrap()
}
fn get_includes_base_dir(config_base_path: &CanonicalizedPathBuf, file_patterns: &GlobPatterns) -> CanonicalizedPathBuf {
let mut base_dir = config_base_path.clone();
for pattern in file_patterns.config_includes.iter().flatten() {
if !is_negated_glob(&pattern.relative_pattern) && base_dir.starts_with(&pattern.base_dir) {
base_dir = pattern.base_dir.clone();
}
}
base_dir
}
fn get_plugin_patterns<'a>(plugins: impl Iterator<Item = &'a PluginWithConfig>) -> Vec<String> {
let mut file_names = HashSet::new();
let mut file_exts = HashSet::new();
let mut association_globs = Vec::new();
for plugin in plugins {
file_names.extend(&plugin.file_matching.file_names);
file_exts.extend(&plugin.file_matching.file_extensions);
if let Some(associations) = plugin.associations.as_ref() {
for pattern in process_config_patterns(associations) {
if !is_negated_glob(&pattern) {
association_globs.push(pattern);
}
}
}
}
let mut result = Vec::new();
if !file_exts.is_empty() {
result.push(format!("**/*.{{{}}}", file_exts.into_iter().map(|s| s.as_str()).collect::<Vec<_>>().join(",")));
}
if !file_names.is_empty() {
result.push(format!("**/{{{}}}", file_names.into_iter().map(|s| s.as_str()).collect::<Vec<_>>().join(",")));
}
result.extend(association_globs);
result
}