use std::collections::BTreeSet;
use std::io::{self, ErrorKind};
use std::path::{Path, PathBuf};
use serde_json::Value;
#[derive(Debug, Default)]
pub struct ComposerPackages {
names: BTreeSet<String>,
}
impl ComposerPackages {
pub fn read(root: &Path) -> io::Result<Self> {
for relative in
["vendor/composer/installed.json", "composer.lock", "composer.json"]
{
let path = root.join(relative);
let bytes = match std::fs::read(&path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == ErrorKind::NotFound => continue,
Err(error) => return Err(error),
};
let document: Value =
serde_json::from_slice(&bytes).map_err(|error| {
io::Error::new(ErrorKind::InvalidData, error)
})?;
let names = if relative == "composer.json" {
document
.get("require")
.and_then(Value::as_object)
.into_iter()
.flat_map(|packages| packages.keys().cloned())
.collect()
} else {
document
.get("packages")
.unwrap_or(&document)
.as_array()
.ok_or_else(|| {
io::Error::new(
ErrorKind::InvalidData,
format!(
"{}: expected Composer packages",
path.display()
),
)
})?
.iter()
.filter_map(|package| package.get("name")?.as_str())
.map(str::to_owned)
.collect()
};
return Ok(Self { names });
}
Ok(Self::default())
}
pub fn contains(
&self,
package: &str,
) -> bool {
self.names.contains(package)
}
pub fn frameworks(&self) -> Vec<String> {
[
("laravel/framework", "Laravel"),
("symfony/framework-bundle", "Symfony"),
]
.into_iter()
.filter(|(package, _)| self.contains(package))
.map(|(_, label)| label.to_owned())
.collect()
}
}
pub fn project_root(entry: &Path) -> io::Result<Option<PathBuf>> {
let entry = if entry.is_absolute() {
entry.to_owned()
} else {
std::env::current_dir()?.join(entry)
};
for directory in entry.parent().into_iter().flat_map(Path::ancestors) {
for relative in
["composer.json", "composer.lock", "vendor/composer/installed.json"]
{
if directory.join(relative).try_exists()? {
return Ok(Some(directory.to_owned()));
}
}
}
Ok(None)
}
pub(crate) fn detect(entry: &Path) -> Vec<String> {
let discovered = project_root(entry).and_then(|root| {
root.map(|root| ComposerPackages::read(&root)).transpose()
});
match discovered {
Ok(packages) => {
packages.map(|packages| packages.frameworks()).unwrap_or_default()
}
Err(error) => {
tracing::warn!(
entry = %entry.display(),
%error,
"PHP framework metadata unavailable; execution is unchanged"
);
Vec::new()
}
}
}