use php_ast::ast::{BinaryOp, MagicConstKind};
use php_ast::owned::visitor::OwnedVisitor;
use php_ast::owned::{Expr, ExprKind};
use std::ops::ControlFlow;
use std::path::{Component, Path, PathBuf};
use crate::file_extensions::PhpFileExtensions;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VendorTargets {
Skip,
Follow,
}
pub fn include_closure(
seeds: Vec<PathBuf>,
vendor: VendorTargets,
extensions: &PhpFileExtensions,
) -> Vec<PathBuf> {
let mut out = seeds;
let mut seen: rustc_hash::FxHashSet<PathBuf> = out.iter().cloned().collect();
let mut queue: Vec<PathBuf> = out.clone();
while let Some(file) = queue.pop() {
let Some(dir) = file.parent().map(Path::to_path_buf) else {
continue;
};
let Ok(text) = crate::read_php_source(&file) else {
continue;
};
if !text.contains("require") && !text.contains("include") {
continue;
}
let parsed = php_rs_parser::parse(&text);
let mut scanner = IncludeTargetScanner {
file: &file,
targets: Vec::new(),
};
let _ = scanner.visit_program(&parsed.program);
for target in scanner.targets {
let resolved = if Path::new(&target).is_absolute() {
PathBuf::from(target)
} else {
dir.join(target)
};
let resolved = lexically_normalize(&resolved);
if !extensions.is_php_source(&resolved) {
continue;
}
if vendor == VendorTargets::Skip
&& resolved.components().any(|c| c.as_os_str() == "vendor")
{
continue;
}
if !resolved.is_file() {
continue;
}
if seen.insert(resolved.clone()) {
out.push(resolved.clone());
queue.push(resolved);
}
}
}
out
}
fn lexically_normalize(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(_)) = components.peek().copied() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(_) => unreachable!(),
Component::RootDir => ret.push(component.as_os_str()),
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => ret.push(c),
}
}
ret
}
struct IncludeTargetScanner<'a> {
file: &'a Path,
targets: Vec<String>,
}
impl OwnedVisitor for IncludeTargetScanner<'_> {
fn visit_expr(&mut self, expr: &Expr) -> ControlFlow<()> {
if let ExprKind::Include(_, inner) = &expr.kind {
if let Some(target) = resolve_static_include_target(inner, self.file) {
self.targets.push(target);
}
}
crate::recursion::walk_expr(self, expr)
}
}
fn resolve_static_include_target(expr: &Expr, file: &Path) -> Option<String> {
match &expr.kind {
ExprKind::String(s) => Some(s.to_string()),
ExprKind::MagicConst(MagicConstKind::File) => Some(file.to_string_lossy().into_owned()),
ExprKind::MagicConst(MagicConstKind::Dir) => {
Some(file.parent()?.to_string_lossy().into_owned())
}
ExprKind::Parenthesized(inner) => resolve_static_include_target(inner, file),
ExprKind::Binary(b) if b.op == BinaryOp::Concat => {
let left = resolve_static_include_target(&b.left, file)?;
let right = resolve_static_include_target(&b.right, file)?;
Some(format!("{left}{right}"))
}
ExprKind::FunctionCall(call) => {
let ExprKind::Identifier(name) = &call.name.kind else {
return None;
};
if !name.eq_ignore_ascii_case("dirname") || call.args.len() > 2 {
return None;
}
let path = resolve_static_include_target(call.args.first()?.value.as_ref()?, file)?;
let levels = match call.args.get(1) {
None => 1,
Some(arg) => match arg.value.as_ref()?.kind {
ExprKind::Int(n) if n >= 1 => n,
_ => return None,
},
};
let mut path = Path::new(&path);
for _ in 0..levels {
path = path.parent()?;
}
Some(path.to_string_lossy().into_owned())
}
_ => None,
}
}