#[allow(clippy::wildcard_imports, reason = "many AST node variants used")]
use oxc_ast::ast::*;
use rustc_hash::FxHashSet;
use super::visit_chain_names::MAX_RECORDED_CHAIN_LINKS;
pub(super) fn flatten_member_path(expr: &Expression<'_>) -> Option<String> {
let mut properties = Vec::new();
let mut expr = expr;
let root = loop {
crate::cost::walk_step();
if properties.len() > MAX_RECORDED_CHAIN_LINKS {
return None;
}
match expr {
Expression::ParenthesizedExpression(paren) => expr = &paren.expression,
Expression::AwaitExpression(await_expr) => expr = &await_expr.argument,
Expression::Identifier(ident) => break ident.name.as_str(),
Expression::ImportMeta(_) => break "import.meta",
Expression::NewTarget(_) => break "new.target",
Expression::StaticMemberExpression(member) => {
properties.push(member.property.name.as_str());
expr = &member.object;
}
_ => return None,
}
};
Some(join_member_path(root, &properties))
}
pub(in crate::visitor) fn join_member_path(
root: &str,
properties_outermost_first: &[impl AsRef<str>],
) -> String {
let len = root.len()
+ properties_outermost_first
.iter()
.map(|property| property.as_ref().len() + 1)
.sum::<usize>();
let mut path = String::with_capacity(len);
path.push_str(root);
for property in properties_outermost_first.iter().rev() {
path.push('.');
path.push_str(property.as_ref());
}
path
}
fn tainted_source_path(expr: &Expression<'_>) -> Option<String> {
match expr {
Expression::ParenthesizedExpression(paren) => tainted_source_path(&paren.expression),
Expression::AwaitExpression(await_expr) => tainted_source_path(&await_expr.argument),
Expression::StaticMemberExpression(member) => {
if let Some(full) = flatten_member_path(expr)
&& fallow_types::extract::is_public_env_path(&full)
{
return None;
}
flatten_member_path(&member.object)
}
_ => None,
}
}
pub(super) fn push_unique_string(out: &mut Vec<String>, value: String) {
if !out.iter().any(|existing| {
crate::cost::walk_step();
existing == &value
}) {
out.push(value);
}
}
#[cfg(test)]
thread_local! {
static CANDIDATE_SCANS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
pub(super) fn source_path_candidates(expr: &Expression<'_>) -> Vec<String> {
#[cfg(test)]
CANDIDATE_SCANS.with(|scans| scans.set(scans.get() + 1));
let mut out = Vec::new();
if let Some(path) = flatten_member_path(expr) {
push_unique_string(&mut out, path);
}
if let Some(path) = tainted_source_path(expr) {
push_unique_string(&mut out, path);
}
out
}
pub(super) fn binding_source_path_candidates(expr: &Expression<'_>) -> Vec<String> {
let mut out = UniquePaths::default();
collect_binding_source_path_candidates(expr, &mut out);
out.paths
}
#[derive(Default)]
struct UniquePaths {
paths: Vec<String>,
seen: FxHashSet<String>,
}
impl UniquePaths {
fn push(&mut self, path: String) {
if self.seen.insert(path.clone()) {
self.paths.push(path);
}
}
}
fn collect_binding_source_path_candidates(expr: &Expression<'_>, out: &mut UniquePaths) {
let mut expr = expr;
loop {
crate::cost::walk_step();
expr = match expr {
Expression::ParenthesizedExpression(paren) => &paren.expression,
Expression::TSAsExpression(ts_as) => &ts_as.expression,
Expression::TSSatisfiesExpression(ts_sat) => &ts_sat.expression,
Expression::TSNonNullExpression(ts_non_null) => &ts_non_null.expression,
Expression::AwaitExpression(await_expr) => &await_expr.argument,
_ => break,
};
}
for path in source_path_candidates(expr) {
out.push(path);
}
match expr {
Expression::TemplateLiteral(template) => {
for expression in &template.expressions {
collect_binding_source_path_candidates(expression, out);
}
}
Expression::BinaryExpression(binary)
if binary.operator == oxc_ast::ast::BinaryOperator::Addition =>
{
collect_binding_source_path_candidates(&binary.left, out);
collect_binding_source_path_candidates(&binary.right, out);
}
Expression::ObjectExpression(object) => {
for property in &object.properties {
let ObjectPropertyKind::ObjectProperty(property) = property else {
continue;
};
collect_binding_source_path_candidates(&property.value, out);
}
}
_ => {}
}
}
#[cfg(all(test, not(miri)))]
mod tests {
use super::*;
fn with_expression<R>(source: &str, check: impl FnOnce(&Expression<'_>) -> R) -> R {
let allocator = oxc_allocator::Allocator::default();
let parsed =
oxc_parser::Parser::new(&allocator, source, oxc_span::SourceType::mjs()).parse();
assert!(parsed.diagnostics.is_empty(), "the fixture must parse");
let Some(Statement::ExpressionStatement(statement)) = parsed.program.body.first() else {
panic!("expected an expression statement");
};
check(&statement.expression)
}
#[test]
fn flatten_member_path_builds_a_chain_at_the_cap() {
let links = ".b".repeat(MAX_RECORDED_CHAIN_LINKS);
let path = with_expression(&format!("a{links};"), flatten_member_path);
assert_eq!(path, Some(format!("a{links}")));
}
#[test]
fn flatten_member_path_refuses_a_chain_past_the_cap() {
let links = ".b".repeat(MAX_RECORDED_CHAIN_LINKS + 1);
assert_eq!(
with_expression(&format!("a{links};"), flatten_member_path),
None
);
}
#[test]
fn concat_operands_give_distinct_candidates_in_first_seen_order() {
let candidates = with_expression(
"req.query + `${req.body}` + req.query + req.body;",
binding_source_path_candidates,
);
assert_eq!(candidates, ["req.query", "req", "req.body"]);
}
#[test]
fn awaits_around_a_source_give_its_candidates_in_one_scan() {
let plain = with_expression("req.query;", binding_source_path_candidates);
CANDIDATE_SCANS.with(|scans| scans.set(0));
let awaited = with_expression(
&format!("{}(req.query);", "await ".repeat(200)),
binding_source_path_candidates,
);
assert_eq!(awaited, plain);
assert_eq!(plain, ["req.query", "req"]);
assert_eq!(CANDIDATE_SCANS.with(std::cell::Cell::get), 1);
}
}