use anyhow::Result;
use indexmap::IndexMap;
use std::collections::HashMap;
use std::path::Path;
use crate::environment::CanonicalizedPathBuf;
use crate::patterns::OrderedPatternsMatcher;
use crate::resolution::PluginWithConfig;
use crate::utils::get_lowercase_file_extension;
use crate::utils::get_lowercase_file_name;
use crate::utils::get_shebang_line;
use crate::utils::is_shebang_prefix_match;
#[derive(Default)]
pub struct PluginNameResolutionMaps {
plugins: Vec<PluginEntry>,
has_additive: bool,
extension_to_plugin_indexes_map: HashMap<String, Vec<usize>>,
file_name_to_plugin_indexes_map: HashMap<String, Vec<usize>>,
association_indexes: Vec<usize>,
shebang_to_extension: Vec<(String, String)>,
}
impl PluginNameResolutionMaps {
pub fn from_plugins<'a>(
plugins: impl Iterator<Item = &'a PluginWithConfig>,
config_base_path: &CanonicalizedPathBuf,
shebangs: Option<&IndexMap<String, String>>,
) -> Result<Self> {
let mut plugin_name_maps = PluginNameResolutionMaps::default();
if let Some(shebangs) = shebangs {
plugin_name_maps.shebang_to_extension = shebangs.iter().map(|(shebang, extension)| (shebang.clone(), extension.clone())).collect();
plugin_name_maps
.shebang_to_extension
.sort_by_key(|(shebang, _)| std::cmp::Reverse(shebang.len()));
}
for plugin in plugins {
let index = plugin_name_maps.plugins.len();
let additive = plugin.file_matching.additive;
for extension in &plugin.file_matching.file_extensions {
plugin_name_maps
.extension_to_plugin_indexes_map
.entry(extension.to_lowercase())
.or_default()
.push(index);
}
for file_name in &plugin.file_matching.file_names {
plugin_name_maps
.file_name_to_plugin_indexes_map
.entry(file_name.to_lowercase())
.or_default()
.push(index);
}
let associations = get_plugin_association_glob_matcher(plugin, config_base_path)?;
if associations.is_some() && !additive {
plugin_name_maps.association_indexes.push(index);
}
plugin_name_maps.has_additive |= additive;
plugin_name_maps.plugins.push(PluginEntry {
name: plugin.name().to_string(),
additive,
associations,
});
}
Ok(plugin_name_maps)
}
pub fn get_plugin_names_from_file_path<'a>(&'a self, file_path: &Path) -> PathPluginNames<'a> {
let file_name = get_lowercase_file_name(file_path);
let extension = get_lowercase_file_extension(file_path);
let claiming_plugins = self.claiming_plugins(file_path, file_name.as_deref(), extension.as_deref());
PathPluginNames {
has_claiming_plugin: !claiming_plugins.is_empty(),
names: self.get_plugin_names(&claiming_plugins, file_path, file_name.as_deref(), extension.as_deref()),
}
}
pub fn get_plugin_names_from_file_path_and_bytes<'a>(&'a self, file_path: &Path, file_bytes_start: &[u8]) -> Vec<&'a str> {
let path_plugin_names = self.get_plugin_names_from_file_path(file_path);
if path_plugin_names.has_claiming_plugin() {
return path_plugin_names.into_names();
}
self
.get_plugin_names_from_shebang(file_path, file_bytes_start)
.unwrap_or_else(|| path_plugin_names.into_names())
}
pub fn may_match_shebang(&self, file_path: &Path) -> bool {
!self.shebang_to_extension.is_empty() && file_path.extension().is_none()
}
pub fn get_plugin_names_from_shebang<'a>(&'a self, file_path: &Path, file_bytes_start: &[u8]) -> Option<Vec<&'a str>> {
if !self.may_match_shebang(file_path) {
return None;
}
let shebang = get_shebang_line(file_bytes_start)?;
let extension = self
.shebang_to_extension
.iter()
.find(|(configured_shebang, _)| is_shebang_prefix_match(shebang, configured_shebang))
.map(|(_, extension)| extension)?;
let file_name = get_lowercase_file_name(file_path);
let claiming_plugins = self.claiming_plugins(file_path, None, Some(extension));
Some(self.get_plugin_names(&claiming_plugins, file_path, file_name.as_deref(), Some(extension)))
}
fn get_plugin_names<'a>(&'a self, claiming_plugins: &ClaimingPlugins, file_path: &Path, file_name: Option<&str>, extension: Option<&str>) -> Vec<&'a str> {
if !self.has_additive {
return claiming_plugins.indexes().iter().map(|&index| self.plugins[index].name.as_str()).collect();
}
self
.plugins
.iter()
.enumerate()
.filter(|(index, plugin)| {
if plugin.additive {
self.matches_file(*index, file_path, file_name, extension)
} else {
claiming_plugins.contains(*index)
}
})
.map(|(_, plugin)| plugin.name.as_str())
.collect()
}
fn claiming_plugins(&self, file_path: &Path, file_name: Option<&str>, extension: Option<&str>) -> ClaimingPlugins {
let association_indexes = self
.association_indexes
.iter()
.copied()
.filter(|&index| self.matches_associations(index, file_path))
.collect::<Vec<_>>();
if !association_indexes.is_empty() {
return ClaimingPlugins::Many(association_indexes);
}
let index = file_name
.and_then(|file_name| self.first_claiming_index(&self.file_name_to_plugin_indexes_map, file_name, file_path))
.or_else(|| extension.and_then(|extension| self.first_claiming_index(&self.extension_to_plugin_indexes_map, extension, file_path)));
match index {
Some(index) => ClaimingPlugins::One(index),
None => ClaimingPlugins::None,
}
}
fn first_claiming_index(&self, map: &HashMap<String, Vec<usize>>, key: &str, file_path: &Path) -> Option<usize> {
map
.get(key)?
.iter()
.copied()
.find(|&index| !self.plugins[index].additive && self.is_not_associations_excluded(index, file_path))
}
fn matches_file(&self, index: usize, file_path: &Path, file_name: Option<&str>, extension: Option<&str>) -> bool {
if self.matches_associations(index, file_path) {
return true;
}
let matches_default = file_name.is_some_and(|file_name| map_contains(&self.file_name_to_plugin_indexes_map, file_name, index))
|| extension.is_some_and(|extension| map_contains(&self.extension_to_plugin_indexes_map, extension, index));
matches_default && self.is_not_associations_excluded(index, file_path)
}
fn matches_associations(&self, index: usize, file_path: &Path) -> bool {
self.plugins[index].associations.as_ref().is_some_and(|matcher| matcher.matches(file_path))
}
fn is_not_associations_excluded(&self, index: usize, file_path: &Path) -> bool {
!self.plugins[index].associations.as_ref().is_some_and(|matcher| matcher.is_excluded(file_path))
}
}
pub struct PathPluginNames<'a> {
names: Vec<&'a str>,
has_claiming_plugin: bool,
}
impl<'a> PathPluginNames<'a> {
pub fn has_claiming_plugin(&self) -> bool {
self.has_claiming_plugin
}
pub fn into_names(self) -> Vec<&'a str> {
self.names
}
}
struct PluginEntry {
name: String,
additive: bool,
associations: Option<OrderedPatternsMatcher>,
}
enum ClaimingPlugins {
None,
One(usize),
Many(Vec<usize>),
}
impl ClaimingPlugins {
pub fn indexes(&self) -> &[usize] {
match self {
ClaimingPlugins::None => &[],
ClaimingPlugins::One(index) => std::slice::from_ref(index),
ClaimingPlugins::Many(indexes) => indexes,
}
}
pub fn is_empty(&self) -> bool {
self.indexes().is_empty()
}
pub fn contains(&self, index: usize) -> bool {
self.indexes().contains(&index)
}
}
fn map_contains(map: &HashMap<String, Vec<usize>>, key: &str, index: usize) -> bool {
map.get(key).is_some_and(|indexes| indexes.contains(&index))
}
fn get_plugin_association_glob_matcher(plugin: &PluginWithConfig, config_base_path: &CanonicalizedPathBuf) -> Result<Option<OrderedPatternsMatcher>> {
match plugin.associations.as_deref() {
Some(associations) => Ok(Some(OrderedPatternsMatcher::new(associations, config_base_path)?)),
None => Ok(None),
}
}