use std::cell::RefCell;
use std::collections::BTreeMap;
use std::collections::HashSet;
use std::hash::Hasher;
use std::path::Path;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use anyhow::Result;
use anyhow::bail;
use dprint_core::async_runtime::FutureExt;
use dprint_core::async_runtime::LocalBoxFuture;
use dprint_core::configuration::ConfigKeyMap;
use dprint_core::plugins::CancellationToken;
use dprint_core::plugins::CheckConfigUpdatesMessage;
use dprint_core::plugins::ConfigChange;
use dprint_core::plugins::CriticalFormatError;
use dprint_core::plugins::FileMatchingInfo;
use dprint_core::plugins::FormatConfigId;
use dprint_core::plugins::FormatError;
use dprint_core::plugins::FormatRange;
use dprint_core::plugins::FormatResult;
use dprint_core::plugins::HostFormatRequest;
use dprint_core::plugins::PluginInfo;
use dprint_core::plugins::process::HostFormatCallback;
use indexmap::IndexMap;
use thiserror::Error;
use crate::arg_parser::CliArgs;
use crate::arg_parser::ConfigDiscovery;
use crate::arg_parser::FilePatternArgs;
use crate::configuration::GlobalConfigDiagnostic;
use crate::configuration::RawPluginConfigOverride;
use crate::configuration::ResolveConfigError;
use crate::configuration::ResolvedConfig;
use crate::configuration::ResolvedConfigPathWithText;
use crate::configuration::get_default_config_file_in_ancestor_directories;
use crate::configuration::get_global_config;
use crate::configuration::get_plugin_config_map;
use crate::configuration::inherit_config;
use crate::configuration::resolve_config_from_args;
use crate::configuration::resolve_config_from_path_with_bytes;
use crate::configuration::resolve_global_config_path_and_text;
use crate::environment::CanonicalizedPathBuf;
use crate::environment::Environment;
use crate::format::EnsureStableFormat;
use crate::format::stabilize_format_text;
use crate::paths::FilesPathsByPlugins;
use crate::paths::IncludeScope;
use crate::paths::NoFilesFoundError;
use crate::paths::get_and_resolve_file_paths;
use crate::paths::get_file_paths_by_plugins;
use crate::paths::get_plugin_names_for_file_on_disk;
use crate::patterns::FileMatcher;
use crate::patterns::FileMatcherOptions;
use crate::patterns::OrderedPatternsMatcher;
use crate::plugins::FormatConfig;
use crate::plugins::InitializedPlugin;
use crate::plugins::InitializedPluginFormatRequest;
use crate::plugins::OutputPluginConfigDiagnosticsError;
use crate::plugins::PluginNameResolutionMaps;
use crate::plugins::PluginResolver;
use crate::plugins::PluginWrapper;
use crate::plugins::output_plugin_config_diagnostics;
use crate::utils::FastInsecureHasher;
use crate::utils::GlobOutput;
use crate::utils::OutsideBasePath;
use crate::utils::PathSource;
use crate::utils::escape_glob_text_for_cli;
use crate::utils::is_negated_glob;
pub enum GetPluginResult {
HadDiagnostics(Vec<String>),
Success(InitializedPluginWithConfig),
}
#[derive(Debug, Error)]
#[error("Had {} configuration errors.", .diagnostics.len())]
pub struct PluginConfigDiagnosticsError {
pub diagnostics: Vec<String>,
}
pub struct PluginConfigOverride {
files: Vec<String>,
properties: ConfigKeyMap,
config_id: FormatConfigId,
matcher: OrderedPatternsMatcher,
}
pub struct PluginWithConfig {
pub plugin: Rc<PluginWrapper>,
pub associations: Option<Vec<String>>,
pub overrides: Vec<PluginConfigOverride>,
pub format_config: Arc<FormatConfig>,
pub file_matching: FileMatchingInfo,
serialized_resolved_config: String,
config_diagnostics: tokio::sync::Mutex<Option<Vec<String>>>,
}
pub struct PluginWithConfigOptions {
pub associations: Option<Vec<String>>,
pub format_config: Arc<FormatConfig>,
pub file_matching: FileMatchingInfo,
pub overrides: Vec<PluginConfigOverride>,
pub serialized_resolved_config: String,
}
impl PluginWithConfig {
pub fn new(plugin: Rc<PluginWrapper>, options: PluginWithConfigOptions) -> Self {
Self {
plugin,
associations: options.associations,
overrides: options.overrides,
format_config: options.format_config,
config_diagnostics: Default::default(),
file_matching: options.file_matching,
serialized_resolved_config: options.serialized_resolved_config,
}
}
pub fn incremental_hash(&self, hasher: &mut impl Hasher) {
use std::hash::Hash;
hasher.write(self.info().name.as_bytes());
hasher.write(self.info().version.as_bytes());
let sorted_config = self.format_config.plugin.iter().collect::<BTreeMap<_, _>>();
for (key, value) in sorted_config {
hasher.write(key.as_bytes());
value.hash(hasher);
}
hasher.write(self.serialized_resolved_config.as_bytes());
if let Some(associations) = &self.associations {
associations.len().hash(hasher);
for association in associations {
association.hash(hasher);
}
}
self.overrides.len().hash(hasher);
for override_config in &self.overrides {
override_config.files.len().hash(hasher);
for file in &override_config.files {
file.hash(hasher);
}
let sorted_config = override_config.properties.iter().collect::<BTreeMap<_, _>>();
sorted_config.len().hash(hasher);
for (key, value) in sorted_config {
key.hash(hasher);
value.hash(hasher);
}
}
self.format_config.global.hash(hasher);
}
pub fn get_config_file_overrides_for_path(&self, file_path: &Path) -> ConfigKeyMap {
let mut result = ConfigKeyMap::new();
for override_config in &self.overrides {
if override_config.matcher.matches(file_path) {
for (key, value) in override_config.properties.iter() {
result.insert(key.clone(), value.clone());
}
}
}
result
}
pub fn get_merged_overrides_for_path(&self, file_path: &Path, request_override_config: &ConfigKeyMap) -> ConfigKeyMap {
let mut result = self.get_config_file_overrides_for_path(file_path);
for (key, value) in request_override_config.iter() {
result.insert(key.clone(), value.clone());
}
result
}
pub fn name(&self) -> &str {
&self.info().name
}
pub fn info(&self) -> &PluginInfo {
self.plugin.info()
}
pub async fn initialize(self: &Rc<Self>) -> Result<InitializedPluginWithConfig> {
let instance = self.plugin.initialize().await?;
Ok(InitializedPluginWithConfig {
instance,
plugin: self.clone(),
})
}
pub async fn get_or_create_checking_config_diagnostics<TEnvironment: Environment>(self: &Rc<Self>, environment: &TEnvironment) -> Result<GetPluginResult> {
let instance = self.initialize().await?;
let mut config_diagnostics = self.config_diagnostics.lock().await;
match &*config_diagnostics {
Some(diagnostics) => {
if !diagnostics.is_empty() {
return Ok(GetPluginResult::HadDiagnostics(diagnostics.clone()));
}
Ok(GetPluginResult::Success(instance))
}
None => {
let result = instance.output_config_diagnostics(environment).await?;
if let Err(err) = result {
log_error!(environment, &err.to_string());
*config_diagnostics = Some(err.diagnostics.clone());
Ok(GetPluginResult::HadDiagnostics(err.diagnostics))
} else {
let result = instance.output_override_config_diagnostics(environment).await?;
if let Err(err) = result {
log_error!(environment, &err.to_string());
*config_diagnostics = Some(err.diagnostics.clone());
Ok(GetPluginResult::HadDiagnostics(err.diagnostics))
} else {
*config_diagnostics = Some(Vec::new());
Ok(GetPluginResult::Success(instance))
}
}
}
}
}
}
pub struct InitializedPluginWithConfigFormatRequest {
pub file_path: PathBuf,
pub file_bytes: Vec<u8>,
pub range: FormatRange,
pub override_config: ConfigKeyMap,
pub on_host_format: HostFormatCallback,
pub token: Arc<dyn CancellationToken>,
}
#[derive(Clone)]
pub struct InitializedPluginWithConfig {
plugin: Rc<PluginWithConfig>,
instance: Rc<dyn InitializedPlugin>,
}
impl InitializedPluginWithConfig {
pub fn info(&self) -> &PluginInfo {
self.plugin.info()
}
pub async fn resolved_config(&self) -> Result<String> {
self.instance.resolved_config(self.plugin.format_config.clone()).await
}
pub async fn file_matching_info(&self) -> Result<FileMatchingInfo> {
self.instance.file_matching_info(self.plugin.format_config.clone()).await
}
pub async fn license_text(&self) -> Result<String> {
self.instance.license_text().await
}
pub async fn output_config_diagnostics<TEnvironment: Environment>(
&self,
environment: &TEnvironment,
) -> Result<Result<(), OutputPluginConfigDiagnosticsError>> {
output_plugin_config_diagnostics(&self.info().name, &*self.instance, self.plugin.format_config.clone(), environment).await
}
pub async fn output_override_config_diagnostics<TEnvironment: Environment>(
&self,
environment: &TEnvironment,
) -> Result<Result<(), OutputPluginConfigDiagnosticsError>> {
let mut diagnostics = Vec::new();
for override_config in &self.plugin.overrides {
let mut plugin_config = self.plugin.format_config.plugin.clone();
for (key, value) in override_config.properties.iter() {
plugin_config.insert(key.clone(), value.clone());
}
let format_config = Arc::new(FormatConfig {
id: override_config.config_id,
plugin: plugin_config,
global: self.plugin.format_config.global.clone(),
});
for diagnostic in self.instance.config_diagnostics(format_config).await? {
let message = format!("[{}]: {}", self.info().name, diagnostic);
log_warn!(environment, "{}", message);
diagnostics.push(message);
}
}
if !diagnostics.is_empty() {
Ok(Err(OutputPluginConfigDiagnosticsError {
plugin_name: self.info().name.to_string(),
diagnostic_count: diagnostics.len(),
diagnostics,
}))
} else {
Ok(Ok(()))
}
}
pub async fn check_config_updates(&self, message: CheckConfigUpdatesMessage) -> Result<Vec<ConfigChange>> {
self.instance.check_config_updates(message).await
}
pub async fn format_text(&self, request: InitializedPluginWithConfigFormatRequest) -> FormatResult {
self
.instance
.format_text(InitializedPluginFormatRequest {
file_path: request.file_path,
file_text: request.file_bytes,
range: request.range,
config: self.plugin.format_config.clone(),
override_config: request.override_config,
on_host_format: request.on_host_format,
token: request.token,
})
.await
}
}
pub struct PluginsScope<TEnvironment: Environment> {
environment: TEnvironment,
pub config: Option<Rc<ResolvedConfig>>,
pub plugins: IndexMap<String, Rc<PluginWithConfig>>,
pub plugin_name_maps: PluginNameResolutionMaps,
global_config_diagnostics: Vec<GlobalConfigDiagnostic>,
cached_editor_file_matcher: RefCell<Option<FileMatcher<TEnvironment>>>,
}
impl<TEnvironment: Environment> PluginsScope<TEnvironment> {
pub fn new(
environment: TEnvironment,
plugins: Vec<Rc<PluginWithConfig>>,
config: Rc<ResolvedConfig>,
global_config_diagnostics: Vec<GlobalConfigDiagnostic>,
) -> Result<Self> {
let plugin_name_maps = PluginNameResolutionMaps::from_plugins(plugins.iter().map(|p| p.as_ref()), &config.base_path, config.shebangs.as_ref())?;
Ok(PluginsScope {
environment,
config: Some(config),
plugin_name_maps,
plugins: plugins.into_iter().map(|p| (p.name().to_string(), p)).collect(),
global_config_diagnostics,
cached_editor_file_matcher: Default::default(),
})
}
pub fn ensure_valid_for_cli_args(&self, cli_args: &CliArgs) -> Result<()> {
self.ensure_no_global_config_diagnostics()?;
self.ensure_plugins_found()?;
if cli_args.plugins.is_empty() {
self.ensure_no_unknown_config_property_diagnostics()?;
}
Ok(())
}
pub fn ensure_plugins_found(&self) -> Result<(), NoPluginsFoundError> {
if self.plugins.is_empty() { Err(NoPluginsFoundError) } else { Ok(()) }
}
pub fn ensure_no_global_config_diagnostics(&self) -> Result<(), ResolveConfigError> {
if self.global_config_diagnostics.is_empty() {
return Ok(());
}
let diagnostics = self
.global_config_diagnostics
.iter()
.filter_map(|d| match d {
GlobalConfigDiagnostic::UnknownProperty(_) => None,
GlobalConfigDiagnostic::Other(d) => Some(d.to_string()),
})
.collect::<Vec<_>>();
self.error_for_diagnostics(&diagnostics)
}
pub fn ensure_no_unknown_config_property_diagnostics(&self) -> Result<(), ResolveConfigError> {
if self.global_config_diagnostics.is_empty() {
return Ok(());
}
let diagnostics = self
.global_config_diagnostics
.iter()
.filter_map(|d| match d {
GlobalConfigDiagnostic::UnknownProperty(d) => Some(d.to_string()),
GlobalConfigDiagnostic::Other(_) => None,
})
.collect::<Vec<_>>();
self.error_for_diagnostics(&diagnostics)
}
fn error_for_diagnostics(&self, diagnostics: &[String]) -> Result<(), ResolveConfigError> {
if diagnostics.is_empty() {
return Ok(());
}
let diagnostics_len = diagnostics.len();
let mut output_text = String::new();
for diagnostic in diagnostics {
output_text.push_str("* ");
output_text.push_str(diagnostic);
output_text.push('\n');
}
output_text.push_str(&format!("\nHad {} config diagnostic(s)", diagnostics_len));
if let Some(config) = &self.config {
output_text.push_str(&format!(" in {}", config.source));
}
Err(ResolveConfigError::Other(anyhow::anyhow!("{}", output_text)))
}
pub fn process_plugin_count(&self) -> usize {
self.plugins.values().filter(|p| p.plugin.is_process_plugin()).count()
}
pub fn get_plugin(&self, name: &str) -> Rc<PluginWithConfig> {
self
.plugins
.get(name)
.cloned()
.unwrap_or_else(|| panic!("Expected to find plugin in collection: {}", name))
}
pub fn plugins_hash(&self) -> u64 {
use std::hash::Hash;
let mut hasher = FastInsecureHasher::default();
for plugin in self.plugins.values() {
plugin.incremental_hash(&mut hasher);
}
if let Some(shebangs) = self.config.as_ref().and_then(|c| c.shebangs.as_ref()) {
shebangs.len().hash(&mut hasher);
for (shebang, extension) in shebangs {
shebang.hash(&mut hasher);
extension.hash(&mut hasher);
}
}
hasher.finish()
}
pub fn create_host_format_callback(self: &Rc<Self>) -> HostFormatCallback {
let scope = self.clone();
Rc::new(move |host_request| scope.format(host_request))
}
pub fn can_format_for_editor(&self, file_path: &Path, file_bytes_start: Option<&[u8]>) -> bool {
if !self.matches_editor_file_patterns(file_path) {
return false;
}
if self.plugin_name_maps.may_match_shebang(file_path) {
return match file_bytes_start {
Some(file_bytes_start) => !self
.plugin_name_maps
.get_plugin_names_from_file_path_and_bytes(file_path, file_bytes_start)
.is_empty(),
None => !get_plugin_names_for_file_on_disk(&self.plugin_name_maps, file_path, &self.environment).is_empty(),
};
}
true
}
fn matches_editor_file_patterns(&self, file_path: &Path) -> bool {
let mut file_matcher_borrow = self.cached_editor_file_matcher.borrow_mut();
if file_matcher_borrow.is_none() {
let Some(config) = &self.config else {
return false;
};
let matcher = match FileMatcher::new(
self.environment.clone(),
FileMatcherOptions {
config,
args: &FilePatternArgs::default(),
root_dir: &config.base_path,
specified_file_path: None,
detect_gitignore_changes: true,
},
) {
Ok(matcher) => matcher,
Err(err) => {
log_warn!(self.environment, "Error creating file matcher: {}", err);
return false;
}
};
file_matcher_borrow.replace(matcher);
}
match file_matcher_borrow.as_mut() {
Some(file_matcher) => file_matcher.matches_and_dir_not_ignored(file_path),
None => false, }
}
pub fn format(self: &Rc<Self>, request: HostFormatRequest) -> LocalBoxFuture<'static, FormatResult> {
let plugin_names = self
.plugin_name_maps
.get_plugin_names_from_file_path_and_bytes(&request.file_path, &request.file_bytes)
.into_iter()
.map(ToOwned::to_owned)
.collect::<Vec<String>>();
log_debug!(
self.environment,
"Host formatting {} - File length: {} - Plugins: [{}] - Range: {:?}",
request.file_path.display(),
request.file_bytes.len(),
plugin_names.join(", "),
request.range,
);
let scope = self.clone();
async move {
let mut file_text = request.file_bytes;
let mut had_change = false;
for plugin_name in plugin_names {
let plugin = scope.get_plugin(&plugin_name);
match plugin.get_or_create_checking_config_diagnostics(&scope.environment).await {
Ok(GetPluginResult::Success(initialized_plugin)) => {
let result = initialized_plugin
.format_text(InitializedPluginWithConfigFormatRequest {
file_path: request.file_path.clone(),
file_bytes: file_text.clone(),
range: request.range.clone(),
override_config: plugin.get_merged_overrides_for_path(&request.file_path, &request.override_config),
on_host_format: scope.create_host_format_callback(),
token: request.token.clone(),
})
.await;
if let Some(new_text) = result? {
file_text = new_text;
had_change = true;
}
}
Ok(GetPluginResult::HadDiagnostics(diagnostics)) => return Err(FormatError::new(PluginConfigDiagnosticsError { diagnostics })),
Err(err) => return Err(CriticalFormatError(FormatError::new(err)).into()),
}
}
Ok(if had_change { Some(file_text) } else { None })
}
.boxed_local()
}
pub async fn format_stable(self: &Rc<Self>, request: HostFormatRequest, ensure_stable_format: EnsureStableFormat) -> FormatResult {
if !ensure_stable_format.0 || request.range.is_some() {
return self.format(request).await;
}
let file_path = request.file_path.clone();
let override_config = request.override_config.clone();
let token = request.token.clone();
let original_text = request.file_bytes.clone();
let Some(formatted_text) = self.format(request).await? else {
return Ok(None);
};
if formatted_text == original_text {
return Ok(Some(formatted_text));
}
let stable_text = stabilize_format_text(&self.environment, &file_path, formatted_text, |text| {
let scope = self.clone();
let request = HostFormatRequest {
file_path: file_path.clone(),
file_bytes: text.clone(),
range: None,
override_config: override_config.clone(),
token: token.clone(),
};
async move {
if request.token.is_cancelled() {
return Ok(text);
}
match scope.format(request).await {
Ok(new_text) => Ok(new_text.unwrap_or(text)),
Err(err) => Err(anyhow::anyhow!(dprint_core::plugins::error_to_string(&err))),
}
}
})
.await
.map_err(FormatError::new)?;
Ok(Some(stable_text))
}
}
pub struct PluginsScopeAndPathsCollection<TEnvironment: Environment> {
environment: TEnvironment,
inner: Vec<PluginsScopeAndPaths<TEnvironment>>,
}
impl<TEnvironment: Environment> PluginsScopeAndPathsCollection<TEnvironment> {
pub fn ensure_valid_for_cli_args(&self, cli_args: &CliArgs) -> Result<()> {
for scope in &self.inner {
scope.scope.ensure_valid_for_cli_args(cli_args)?;
}
if !cli_args.sub_command.allow_no_files() {
let cli_file_patterns = cli_args.sub_command.file_patterns().and_then(|p| p.include_patterns.as_ref());
match cli_file_patterns {
Some(patterns) if patterns.is_empty() => {}
Some(_) => {
let all_empty = self.iter().all(|s| s.file_paths_by_plugins.is_empty());
if all_empty {
return Err(
NoFilesFoundError {
base_path: self.environment.cwd(),
}
.into(),
);
}
}
None => {
for scope in &self.inner {
if let Some(config) = scope.scope.config.as_ref() {
scope.file_paths_by_plugins.ensure_not_empty(&config.base_path)?;
}
}
}
}
}
Ok(())
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn iter(&self) -> impl Iterator<Item = &PluginsScopeAndPaths<TEnvironment>> {
self.inner.iter()
}
pub fn into_iter(self) -> impl Iterator<Item = PluginsScopeAndPaths<TEnvironment>> {
self.inner.into_iter()
}
}
pub struct PluginsScopeAndPaths<TEnvironment: Environment> {
pub scope: PluginsScope<TEnvironment>,
pub file_paths_by_plugins: FilesPathsByPlugins,
}
pub struct ResolvePluginsScopeAndPathsOptions {
pub skip_traversal: bool,
}
pub async fn resolve_plugins_scope_and_paths<TEnvironment: Environment>(
args: &CliArgs,
patterns: &FilePatternArgs,
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
options: ResolvePluginsScopeAndPathsOptions,
) -> Result<PluginsScopeAndPathsCollection<TEnvironment>> {
let resolver = PluginsAndPathsResolver {
args,
patterns,
environment,
plugin_resolver,
skip_traversal: options.skip_traversal,
};
resolver.resolve_for_config().await
}
struct PluginsAndPathsResolver<'a, TEnvironment: Environment> {
args: &'a CliArgs,
patterns: &'a FilePatternArgs,
environment: &'a TEnvironment,
plugin_resolver: &'a Rc<PluginResolver<TEnvironment>>,
skip_traversal: bool,
}
impl<'a, TEnvironment: Environment> PluginsAndPathsResolver<'a, TEnvironment> {
pub async fn resolve_for_config(&'a self) -> Result<PluginsScopeAndPathsCollection<TEnvironment>> {
let config = Rc::new(resolve_config_from_args(self.args, self.environment).await?);
let scope = resolve_plugins_scope(config.clone(), self.environment, self.plugin_resolver).await?;
let config_discovery = self.args.config_discovery(self.environment);
let mut glob_output = if self.skip_traversal {
GlobOutput::default()
} else {
get_and_resolve_file_paths(
&config,
self.patterns,
config_discovery,
IncludeScope::AllowAncestors,
scope.plugins.values().map(|p| p.as_ref()),
self.environment,
)
.await?
};
let root_config_path = config.source.maybe_local_path().cloned();
let outside_scopes = self
.resolve_outside_base_paths(&mut glob_output, &config, config_discovery, root_config_path.clone())
.await?;
let file_paths_by_plugins = get_file_paths_by_plugins(&scope.plugin_name_maps, glob_output.file_paths, glob_output.shebang_lines, self.environment)?;
let mut result = vec![PluginsScopeAndPaths { scope, file_paths_by_plugins }];
let patterns = Rc::new(self.patterns.clone());
for config_file_path in glob_output.config_files {
result.extend(
self
.resolve_for_sub_config(config_file_path, config.clone(), config_discovery, root_config_path.clone(), patterns.clone())
.await?,
);
}
result.extend(outside_scopes);
Ok(PluginsScopeAndPathsCollection {
environment: self.environment.clone(),
inner: result,
})
}
async fn resolve_outside_base_paths(
&'a self,
glob_output: &mut GlobOutput,
config: &Rc<ResolvedConfig>,
config_discovery: ConfigDiscovery,
root_config_path: Option<CanonicalizedPathBuf>,
) -> Result<Vec<PluginsScopeAndPaths<TEnvironment>>> {
let outside_base_paths = std::mem::take(&mut glob_output.outside_base_paths);
if outside_base_paths.is_empty() {
return Ok(Vec::new());
}
let mut path_groups: IndexMap<OutsideScopeConfigKey, (OutsideScopeConfig, Vec<String>)> = IndexMap::new();
for outside_path in outside_base_paths {
let Some(scope_config) = self.resolve_outside_scope_config(&outside_path, config, config_discovery)? else {
continue; };
path_groups
.entry(scope_config.group_key())
.or_insert_with(|| (scope_config, Vec::new()))
.1
.push(outside_path.include_pattern);
}
let mut result = Vec::new();
for (_, (scope_config, include_patterns)) in path_groups {
result.extend(
self
.resolve_outside_scope(scope_config, include_patterns, config, config_discovery, root_config_path.clone())
.await?,
);
}
Ok(result)
}
fn resolve_outside_scope_config(
&self,
outside_path: &OutsideBasePath,
config: &ResolvedConfig,
config_discovery: ConfigDiscovery,
) -> Result<Option<OutsideScopeConfig>> {
let discover_tree_configs = self.args.config.is_none() && config_discovery.traverse_ancestors();
if discover_tree_configs && let Some(config_path) = get_default_config_file_in_ancestor_directories(self.environment, &outside_path.config_search_dir)? {
return Ok(Some(OutsideScopeConfig::ConfigFile(config_path)));
}
if self.args.config.is_some() || config.is_global {
let root_dir = self.canonical_path_root_dir(&outside_path.config_search_dir)?;
return Ok(Some(OutsideScopeConfig::RebasedCurrentConfig(root_dir)));
}
if matches!(config_discovery, ConfigDiscovery::Default)
&& let Some(config_path) = self.global_config_path_based_at_path_root(&outside_path.config_search_dir)?
{
return Ok(Some(OutsideScopeConfig::ConfigFile(config_path)));
}
if self.args.sub_command.allow_skipping_paths() {
log_warn!(
self.environment,
"WARNING: Skipping '{}' because no dprint config file was found for it.",
outside_path.include_pattern,
);
Ok(None)
} else {
bail!(
concat!(
"No dprint config file found for '{}'. The path is outside the config file's directory ",
"and no dprint config file was found in the path's ancestor directories. Create one there ",
"or set up a global config file by running `dprint init --global`."
),
outside_path.include_pattern,
);
}
}
async fn resolve_outside_scope(
&'a self,
scope_config: OutsideScopeConfig,
mut include_patterns: Vec<String>,
config: &Rc<ResolvedConfig>,
config_discovery: ConfigDiscovery,
root_config_path: Option<CanonicalizedPathBuf>,
) -> Result<Vec<PluginsScopeAndPaths<TEnvironment>>> {
include_patterns.extend(self.patterns.include_patterns.iter().flatten().filter(|p| is_negated_glob(p)).cloned());
include_patterns.push(format!("!{}/**", escape_glob_text_for_cli(&config.base_path.to_string_lossy())));
let patterns = Rc::new(FilePatternArgs {
include_patterns: Some(include_patterns),
only_staged: false,
only_dirty: false,
..self.patterns.clone()
});
match scope_config {
OutsideScopeConfig::ConfigFile(config_path) => {
self
.resolve_for_config_path(
config_path,
config.clone(),
false,
config_discovery,
root_config_path,
patterns,
)
.await
}
OutsideScopeConfig::RebasedCurrentConfig(base_path) => {
let config_discovery = if self.args.config.is_some() {
ConfigDiscovery::IgnoreDescendants
} else {
config_discovery
};
self
.resolve_for_rebased_config(config, base_path, config_discovery, root_config_path, patterns)
.await
}
}
}
async fn resolve_for_rebased_config(
&'a self,
config: &Rc<ResolvedConfig>,
base_path: CanonicalizedPathBuf,
config_discovery: ConfigDiscovery,
root_config_path: Option<CanonicalizedPathBuf>,
patterns: Rc<FilePatternArgs>,
) -> Result<Vec<PluginsScopeAndPaths<TEnvironment>>> {
let mut rebased_config = (**config).clone();
rebased_config.base_path = base_path;
self
.resolve_scope_and_descendants(Rc::new(rebased_config), config_discovery, root_config_path, patterns)
.await
}
fn global_config_path_based_at_path_root(&self, path: &Path) -> Result<Option<ResolvedConfigPathWithText>> {
let Some(global_config_path) = resolve_global_config_path_and_text(self.environment)? else {
return Ok(None);
};
Ok(Some(ResolvedConfigPathWithText {
base_path: self.canonical_path_root_dir(path)?,
..global_config_path
}))
}
fn canonical_path_root_dir(&self, path: &Path) -> Result<CanonicalizedPathBuf> {
let root_dir = path.ancestors().last().unwrap();
Ok(self.environment.canonicalize(root_dir)?)
}
async fn resolve_for_sub_config(
&'a self,
config_file_path: PathBuf,
parent_config: Rc<ResolvedConfig>,
config_discovery: ConfigDiscovery,
root_config_path: Option<CanonicalizedPathBuf>,
patterns: Rc<FilePatternArgs>,
) -> Result<Vec<PluginsScopeAndPaths<TEnvironment>>> {
log_debug!(self.environment, "Analyzing config file {}", config_file_path.display());
let config_file_path = self.environment.canonicalize(&config_file_path)?;
if Some(&config_file_path) == root_config_path.as_ref() {
return Ok(Vec::new());
}
let config_path = ResolvedConfigPathWithText {
content: self.environment.read_file(&config_file_path)?,
base_path: config_file_path.parent().unwrap(),
source: PathSource::new_local(config_file_path),
is_global_config: false,
is_first_download: false,
};
self
.resolve_for_config_path(
config_path,
parent_config,
true,
config_discovery,
root_config_path,
patterns,
)
.await
}
fn resolve_for_config_path(
&'a self,
config_path: ResolvedConfigPathWithText,
parent_config: Rc<ResolvedConfig>,
is_descendant_config: bool,
config_discovery: ConfigDiscovery,
root_config_path: Option<CanonicalizedPathBuf>,
patterns: Rc<FilePatternArgs>,
) -> LocalBoxFuture<'a, Result<Vec<PluginsScopeAndPaths<TEnvironment>>>> {
async move {
let mut config = resolve_config_from_path_with_bytes(&config_path, self.environment).await?;
if is_descendant_config && config.inherit == Some(true) {
config = inherit_config(config, &parent_config)?;
}
if !self.args.plugins.is_empty() {
config.plugins.clone_from(&parent_config.plugins);
}
self
.resolve_scope_and_descendants(Rc::new(config), config_discovery, root_config_path, patterns)
.await
}
.boxed_local()
}
async fn resolve_scope_and_descendants(
&'a self,
config: Rc<ResolvedConfig>,
config_discovery: ConfigDiscovery,
root_config_path: Option<CanonicalizedPathBuf>,
patterns: Rc<FilePatternArgs>,
) -> Result<Vec<PluginsScopeAndPaths<TEnvironment>>> {
let scope = resolve_plugins_scope(config.clone(), self.environment, self.plugin_resolver).await?;
let mut glob_output = get_and_resolve_file_paths(
&config,
&patterns,
config_discovery,
IncludeScope::ConfigDir,
scope.plugins.values().map(|p| p.as_ref()),
self.environment,
)
.await?;
glob_output.outside_base_paths.clear();
let file_paths_by_plugins = get_file_paths_by_plugins(&scope.plugin_name_maps, glob_output.file_paths, glob_output.shebang_lines, self.environment)?;
let mut result = vec![PluginsScopeAndPaths { scope, file_paths_by_plugins }];
for config_file_path in glob_output.config_files {
result.extend(
self
.resolve_for_sub_config(config_file_path, config.clone(), config_discovery, root_config_path.clone(), patterns.clone())
.await?,
);
}
Ok(result)
}
}
enum OutsideScopeConfig {
ConfigFile(ResolvedConfigPathWithText),
RebasedCurrentConfig(CanonicalizedPathBuf),
}
impl OutsideScopeConfig {
fn group_key(&self) -> OutsideScopeConfigKey {
match self {
Self::ConfigFile(config_path) => OutsideScopeConfigKey::ConfigFile(config_path.base_path.clone()),
Self::RebasedCurrentConfig(base_path) => OutsideScopeConfigKey::RebasedCurrentConfig(base_path.clone()),
}
}
}
#[derive(PartialEq, Eq, Hash)]
enum OutsideScopeConfigKey {
ConfigFile(CanonicalizedPathBuf),
RebasedCurrentConfig(CanonicalizedPathBuf),
}
pub async fn get_plugins_scope_from_args<TEnvironment: Environment>(
args: &CliArgs,
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
) -> Result<PluginsScope<TEnvironment>, ResolvePluginsError> {
match resolve_config_from_args(args, environment).await {
Ok(config) => resolve_plugins_scope(Rc::new(config), environment, plugin_resolver).await,
Err(_) => Ok(PluginsScope {
environment: environment.clone(),
config: None,
plugin_name_maps: Default::default(),
plugins: Default::default(),
global_config_diagnostics: Default::default(),
cached_editor_file_matcher: Default::default(),
}),
}
}
#[derive(Debug, Error)]
#[error("No formatting plugins found. Ensure at least one is specified in the 'plugins' array of the configuration file.")]
pub struct NoPluginsFoundError;
#[derive(Debug, Error)]
#[error(transparent)]
pub struct ResolvePluginsError(#[from] anyhow::Error);
pub async fn resolve_plugins_scope<TEnvironment: Environment>(
config: Rc<ResolvedConfig>,
environment: &TEnvironment,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
) -> Result<PluginsScope<TEnvironment>, ResolvePluginsError> {
let plugins = filter_duplicate_plugin_names(plugin_resolver.resolve_plugins(config.plugins.clone()).await?);
let mut config_map = config.config_map.clone();
let mut plugins_with_config = Vec::new();
for plugin in plugins.into_iter() {
plugins_with_config.push((get_plugin_config_map(&plugin, &mut config_map)?, plugin));
}
config_map.retain(|key, _| !config.extended_only_plugin_config_keys.contains(key));
let global_config_result = get_global_config(config_map);
let global_config = global_config_result.config;
let config_base_path = config.base_path.clone();
let plugins = plugins_with_config
.into_iter()
.map(|(plugin_config, plugin)| {
let global_config = global_config.clone();
let overrides = resolve_plugin_config_overrides(plugin_config.overrides, &config_base_path, plugin_resolver)?;
let next_config_id = plugin_resolver.next_config_id();
Ok(
async move {
let instance = plugin.initialize().await?;
let format_config = Arc::new(FormatConfig {
id: next_config_id,
global: global_config,
plugin: plugin_config.properties,
});
let file_matching = instance.file_matching_info(format_config.clone()).await?;
let serialized_resolved_config = instance.resolved_config(format_config.clone()).await?;
Ok::<_, anyhow::Error>(Rc::new(PluginWithConfig::new(
plugin,
PluginWithConfigOptions {
associations: plugin_config.associations,
format_config,
file_matching,
overrides,
serialized_resolved_config,
},
)))
}
.boxed_local(),
)
})
.collect::<Result<Vec<_>>>()?;
let plugin_results = dprint_core::async_runtime::future::join_all(plugins).await;
let mut plugins = Vec::with_capacity(plugin_results.len());
for result in plugin_results {
plugins.push(result?);
}
Ok(PluginsScope::new(environment.clone(), plugins, config, global_config_result.diagnostics)?)
}
fn filter_duplicate_plugin_names(plugins: Vec<Rc<PluginWrapper>>) -> Vec<Rc<PluginWrapper>> {
let mut names = HashSet::with_capacity(plugins.len());
plugins.into_iter().filter(|plugin| names.insert(plugin.info().name.clone())).collect()
}
fn resolve_plugin_config_overrides<TEnvironment: Environment>(
overrides: Vec<RawPluginConfigOverride>,
config_base_path: &CanonicalizedPathBuf,
plugin_resolver: &Rc<PluginResolver<TEnvironment>>,
) -> Result<Vec<PluginConfigOverride>> {
overrides
.into_iter()
.map(|override_config| {
let matcher = OrderedPatternsMatcher::new(&override_config.files, config_base_path)?;
Ok(PluginConfigOverride {
files: override_config.files,
properties: override_config.properties,
config_id: plugin_resolver.next_config_id(),
matcher,
})
})
.collect()
}
#[cfg(test)]
mod test {
use dprint_core::configuration::ConfigKeyValue;
use dprint_core::configuration::GlobalConfiguration;
use crate::plugins::TestPlugin;
use super::*;
#[test]
fn incremental_hash_includes_resolved_config() {
fn hash_with_resolved_config(resolved_config: &str) -> u64 {
let plugin = Rc::new(PluginWrapper::new(Box::new(TestPlugin::new("test-plugin", "test-plugin", vec!["txt"], vec![]))));
let format_config = Arc::new(FormatConfig {
id: FormatConfigId::from_raw(1),
global: Default::default(),
plugin: Default::default(),
});
let plugin_with_config = PluginWithConfig::new(
plugin,
PluginWithConfigOptions {
associations: None,
format_config,
file_matching: FileMatchingInfo {
file_extensions: vec!["txt".to_string()],
file_names: vec![],
additive: false,
},
overrides: Vec::new(),
serialized_resolved_config: resolved_config.to_string(),
},
);
let mut hasher = FastInsecureHasher::default();
plugin_with_config.incremental_hash(&mut hasher);
hasher.finish()
}
assert_ne!(
hash_with_resolved_config(r#"{"cacheKey":"a"}"#),
hash_with_resolved_config(r#"{"cacheKey":"b"}"#)
);
assert_eq!(
hash_with_resolved_config(r#"{"cacheKey":"a"}"#),
hash_with_resolved_config(r#"{"cacheKey":"a"}"#)
);
}
#[test]
fn should_include_shebangs_in_plugins_hash() {
fn hash_with_shebangs(shebangs: Option<IndexMap<String, String>>) -> u64 {
let environment = crate::environment::TestEnvironment::new();
let base_path = CanonicalizedPathBuf::new_for_testing("/");
let config = Rc::new(ResolvedConfig {
config_map: Default::default(),
extended_only_plugin_config_keys: Default::default(),
base_path: base_path.clone(),
source: PathSource::new_local(base_path.join_panic_relative("dprint.json")),
is_global: false,
excludes: None,
includes: None,
incremental: None,
shebangs,
inherit: None,
plugins: Vec::new(),
});
let scope = PluginsScope::new(environment, vec![Rc::new(create_plugin_with_overrides(Vec::new()))], config, Vec::new()).unwrap();
scope.plugins_hash()
}
fn shebangs(entries: &[(&str, &str)]) -> Option<IndexMap<String, String>> {
Some(entries.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect())
}
assert_eq!(hash_with_shebangs(None), hash_with_shebangs(None));
assert_eq!(
hash_with_shebangs(shebangs(&[("#!/bin/sh", "sh")])),
hash_with_shebangs(shebangs(&[("#!/bin/sh", "sh")]))
);
assert_ne!(hash_with_shebangs(None), hash_with_shebangs(shebangs(&[("#!/bin/sh", "sh")])));
assert_ne!(
hash_with_shebangs(shebangs(&[("#!/bin/sh", "sh")])),
hash_with_shebangs(shebangs(&[("#!/bin/sh", "txt")]))
);
}
#[test]
fn should_hash_associations_with_boundaries() {
fn hash_with_associations(associations: Vec<&str>) -> u64 {
let mut plugin = create_plugin_with_overrides(Vec::new());
plugin.associations = Some(associations.into_iter().map(ToOwned::to_owned).collect());
get_plugin_hash(&plugin)
}
assert_ne!(hash_with_associations(vec!["ab", "c"]), hash_with_associations(vec!["a", "bc"]));
}
#[test]
fn should_hash_override_file_patterns_and_property_keys_with_boundaries() {
let plugin_ab_c = create_plugin_with_override(vec!["ab".to_string()], ConfigKeyMap::from([("c".to_string(), ConfigKeyValue::from_bool(true))]));
let plugin_a_bc = create_plugin_with_override(vec!["a".to_string()], ConfigKeyMap::from([("bc".to_string(), ConfigKeyValue::from_bool(true))]));
assert_ne!(get_plugin_hash(&plugin_ab_c), get_plugin_hash(&plugin_a_bc));
}
#[test]
fn should_include_config_overrides_in_incremental_hash() {
let config_base_path = CanonicalizedPathBuf::new_for_testing("/");
let plugin_without_override = create_plugin_with_overrides(Vec::new());
let plugin_with_override = create_plugin_with_overrides(vec![PluginConfigOverride {
files: vec!["**/package.txt".to_string()],
properties: ConfigKeyMap::from([("ending".to_string(), "package".into())]),
config_id: FormatConfigId::from_raw(2),
matcher: OrderedPatternsMatcher::new(&["**/package.txt".to_string()], &config_base_path).unwrap(),
}]);
assert_ne!(get_plugin_hash(&plugin_without_override), get_plugin_hash(&plugin_with_override));
}
fn get_plugin_hash(plugin: &PluginWithConfig) -> u64 {
let mut hasher = FastInsecureHasher::default();
plugin.incremental_hash(&mut hasher);
hasher.finish()
}
fn create_plugin_with_override(files: Vec<String>, properties: ConfigKeyMap) -> PluginWithConfig {
let config_base_path = CanonicalizedPathBuf::new_for_testing("/config");
let matcher = OrderedPatternsMatcher::new(&files, &config_base_path).unwrap();
create_plugin_with_overrides(vec![PluginConfigOverride {
files,
properties,
config_id: FormatConfigId::from_raw(2),
matcher,
}])
}
fn create_plugin_with_overrides(overrides: Vec<PluginConfigOverride>) -> PluginWithConfig {
PluginWithConfig::new(
Rc::new(PluginWrapper::new(Box::new(TestPlugin::new("test-plugin", "test-plugin", vec!["txt"], vec![])))),
PluginWithConfigOptions {
associations: None,
format_config: Arc::new(FormatConfig {
id: FormatConfigId::from_raw(1),
plugin: ConfigKeyMap::from([("ending".to_string(), "base".into())]),
global: GlobalConfiguration::default(),
}),
file_matching: FileMatchingInfo {
file_extensions: vec!["txt".to_string()],
file_names: Vec::new(),
additive: false,
},
overrides,
serialized_resolved_config: String::new(),
},
)
}
}