use anyhow::Result;
use indexmap::IndexMap;
use std::collections::HashMap;
use std::path::Path;
use std::rc::Rc;
use crate::environment::CanonicalizedPathBuf;
use crate::patterns::get_patterns_as_glob_matcher;
use crate::resolution::PluginWithConfig;
use crate::utils::GlobMatcher;
use crate::utils::GlobMatchesDetail;
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 {
extension_to_plugin_names_map: HashMap<String, Vec<String>>,
file_name_to_plugin_names_map: HashMap<String, Vec<String>>,
association_matchers: Vec<(String, Rc<GlobMatcher>)>,
association_matchers_map: HashMap<String, Rc<GlobMatcher>>,
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 plugin_name = plugin.name();
for extension in &plugin.file_matching.file_extensions {
plugin_name_maps
.extension_to_plugin_names_map
.entry(extension.to_lowercase())
.or_default()
.push(plugin_name.to_string());
}
for file_name in &plugin.file_matching.file_names {
plugin_name_maps
.file_name_to_plugin_names_map
.entry(file_name.to_lowercase())
.or_default()
.push(plugin_name.to_string());
}
if let Some(matcher) = get_plugin_association_glob_matcher(plugin, config_base_path)? {
let matcher = Rc::new(matcher);
plugin_name_maps.association_matchers.push((plugin_name.to_string(), matcher.clone()));
plugin_name_maps.association_matchers_map.insert(plugin_name.to_string(), matcher);
}
}
Ok(plugin_name_maps)
}
pub fn get_plugin_names_from_file_path<'a>(&'a self, file_path: &Path) -> Vec<&'a str> {
let mut plugin_names = Vec::new();
for (plugin_name, matcher) in self.association_matchers.iter() {
if matcher.matches(file_path) {
plugin_names.push(plugin_name.as_str());
}
}
if !plugin_names.is_empty() {
return plugin_names;
}
if let Some(file_name) = get_lowercase_file_name(file_path)
&& let Some(plugin_names) = self.file_name_to_plugin_names_map.get(file_name.as_ref())
{
for plugin_name in plugin_names {
if self.is_not_associations_excluded(plugin_name, file_path) {
return vec![plugin_name.as_str()];
}
}
}
if let Some(ext) = get_lowercase_file_extension(file_path)
&& let Some(plugin_names) = self.extension_to_plugin_names_map.get(ext.as_ref())
{
for plugin_name in plugin_names {
if self.is_not_associations_excluded(plugin_name, file_path) {
return vec![plugin_name.as_str()];
}
}
}
plugin_names
}
pub fn get_plugin_names_from_file_path_and_bytes<'a>(&'a self, file_path: &Path, file_bytes_start: &[u8]) -> Vec<&'a str> {
let plugin_names = self.get_plugin_names_from_file_path(file_path);
if !plugin_names.is_empty() {
return plugin_names;
}
self.get_plugin_names_from_shebang(file_path, file_bytes_start)
}
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]) -> Vec<&'a str> {
if !self.may_match_shebang(file_path) {
return Vec::new();
}
let Some(shebang) = get_shebang_line(file_bytes_start) else {
return Vec::new();
};
let Some(extension) = self
.shebang_to_extension
.iter()
.find(|(configured_shebang, _)| is_shebang_prefix_match(shebang, configured_shebang))
.map(|(_, extension)| extension)
else {
return Vec::new();
};
let Some(plugin_names) = self.extension_to_plugin_names_map.get(extension) else {
return Vec::new();
};
for plugin_name in plugin_names {
if self.is_not_associations_excluded(plugin_name, file_path) {
return vec![plugin_name.as_str()];
}
}
Vec::new()
}
fn is_not_associations_excluded(&self, plugin_name: &str, file_path: &Path) -> bool {
match self.association_matchers_map.get(plugin_name) {
Some(matcher) => matcher.matches_detail(file_path) != GlobMatchesDetail::Excluded,
None => true,
}
}
}
fn get_plugin_association_glob_matcher(plugin: &PluginWithConfig, config_base_path: &CanonicalizedPathBuf) -> Result<Option<GlobMatcher>> {
match plugin.associations.as_deref() {
Some(associations) => Ok(Some(get_patterns_as_glob_matcher(associations, config_base_path)?)),
None => Ok(None),
}
}