use super::ast::{
import_has_runtime_bindings, import_module, import_statement_module, is_top_level_variable,
is_type_only_declaration, is_type_only_specifier, string_literal,
};
use super::node_text;
use super::registrations::{registration_callee, RegistrationCallee, RegistrationNames};
use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use tree_sitter::Node;
const COPY_CROSS_DEPTH: usize = 2;
const ASSERTION_MODULES: [&str; 7] = [
"@playwright/test",
"playwright/test",
"@jest/globals",
"expect",
"vitest",
"chai",
"bun:test",
];
pub(super) struct RequireBindings {
module_shadowed: bool,
shadow_ranges: BTreeMap<String, Vec<(usize, usize)>>,
}
impl RequireBindings {
pub(super) fn discover(root: Node<'_>, source: &[u8]) -> Self {
let (scopes, ranges, _) = collect_binding_scopes(root, source);
let shadow_ranges = collect_scoped_bindings(scopes, ranges);
let module_shadowed =
module_runtime_binding_exists(root, source, "require", &shadow_ranges);
Self {
module_shadowed,
shadow_ranges,
}
}
pub(super) fn available(&self, node: Node<'_>) -> bool {
!self.module_shadowed && !position_is_shadowed(&self.shadow_ranges, "require", node)
}
}
#[derive(Default)]
pub(super) struct AssertionBindings {
identifiers: BTreeSet<String>,
namespaces: BTreeSet<String>,
shadow_ranges: BTreeMap<String, Vec<(usize, usize)>>,
mutated_matcher_factories: BTreeSet<String>,
}
impl AssertionBindings {
pub(super) fn discover(
root: Node<'_>,
source: &[u8],
registrations: &RegistrationNames,
configured_modules: &BTreeSet<String>,
) -> Self {
let mut facade_modules = resolved_facade_modules(root, source, registrations);
facade_modules.extend(configured_modules.iter().cloned());
let (scopes, scope_ranges, scope_parents) = collect_binding_scopes(root, source);
let shadow_ranges = collect_scoped_bindings(scopes.clone(), scope_ranges);
let require_shadowed =
module_runtime_binding_exists(root, source, "require", &shadow_ranges);
let alias_writes = namespace_alias_writes(
root,
source,
&scopes,
&scope_parents,
false,
require_shadowed,
);
let mutated_matcher_factories = namespace_alias_writes(
root,
source,
&scopes,
&scope_parents,
true,
require_shadowed,
);
let mut bindings = Self::default();
let mut shadowed = BTreeSet::new();
let mut factory_writes = BTreeSet::new();
let mut stack = vec![root];
while let Some(node) = stack.pop() {
match node.kind() {
"import_statement" if import_has_runtime_bindings(node) => {
let trusted =
bindings.collect_import(node, source, &facade_modules, configured_modules);
extend_untrusted_bindings(node, source, &trusted, &mut shadowed);
}
"variable_declarator" if is_top_level_variable(node) => {
let trusted = if !require_shadowed {
bindings.collect_require(node, source, configured_modules)
} else {
BTreeSet::new()
};
if let Some(name) = node.child_by_field_name("name") {
extend_untrusted_bindings(name, source, &trusted, &mut shadowed);
}
}
"variable_declarator" if is_module_var(node) => {
if let Some(name) = node.child_by_field_name("name") {
collect_binding_names(name, source, &mut shadowed);
}
}
"for_in_statement"
if node.child_by_field_name("left").is_some_and(|left| {
loop_binding_keyword(node, left) == Some("var")
&& nearest_function_scope(node.parent()).is_none()
}) =>
{
if let Some(left) = node.child_by_field_name("left") {
collect_binding_names(left, source, &mut shadowed);
}
}
"function_declaration"
| "generator_function_declaration"
| "class_declaration"
| "abstract_class_declaration"
| "enum_declaration"
if is_module_declaration(node) =>
{
if let Some(name) = node.child_by_field_name("name") {
collect_binding_names(name, source, &mut shadowed);
}
}
"internal_module" | "module"
if !is_erased_declaration(node) && is_module_declaration(node) =>
{
if let Some(name) = node.child_by_field_name("name") {
collect_binding_names(name, source, &mut shadowed);
}
}
_ => {
if let Some(left) = assignment_target(node) {
let mut assigned = BTreeSet::new();
let mut writes = BTreeMap::new();
collect_assignment_targets(left, source, &mut assigned, &mut writes, false);
factory_writes.extend(
writes
.into_keys()
.filter(|name| !position_is_shadowed(&shadow_ranges, name, node)),
);
shadowed.extend(
assigned
.into_iter()
.filter(|name| !position_is_shadowed(&shadow_ranges, name, node)),
);
}
}
}
super::ast::push_named_children_reverse(node, &mut stack);
}
shadowed.extend(factory_writes.intersection(&bindings.namespaces).cloned());
shadowed.extend(alias_writes.intersection(&bindings.namespaces).cloned());
bindings
.identifiers
.retain(|identifier| !shadowed.contains(identifier));
bindings
.namespaces
.retain(|namespace| !shadowed.contains(namespace));
if !shadowed.contains("expect") && !bindings.namespaces.contains("expect") {
bindings.identifiers.insert("expect".to_owned());
}
bindings.shadow_ranges = shadow_ranges;
bindings.mutated_matcher_factories = mutated_matcher_factories;
bindings
}
pub(super) fn is_factory(&self, node: Node<'_>, source: &[u8]) -> bool {
let Some(node) = super::registrations::unwrap_registration_callee(node) else {
return false;
};
if node.kind() == "identifier" {
let name = node_text(node, source);
return self.identifiers.contains(name) && !self.is_locally_shadowed(node, name);
}
let Some(RegistrationCallee::Property { object, name }) = registration_callee(node, source)
else {
return false;
};
let namespace = node_text(object, source);
object.kind() == "identifier"
&& self.namespaces.contains(namespace)
&& !self.is_locally_shadowed(object, namespace)
&& name == "expect"
}
pub(super) fn is_asymmetric_matcher(&self, node: Node<'_>, source: &[u8]) -> bool {
self.asymmetric_matcher(node, source).is_some()
}
pub(super) fn asymmetric_matcher<'source>(
&self,
node: Node<'_>,
source: &'source [u8],
) -> Option<(bool, Cow<'source, str>)> {
if node.kind() != "call_expression" {
return None;
}
let function = node
.child_by_field_name("function")
.and_then(super::registrations::unwrap_registration_callee)?;
let RegistrationCallee::Property {
mut object,
name: matcher,
} = registration_callee(function, source)?
else {
return None;
};
if !matches!(
matcher.as_ref(),
"objectContaining"
| "arrayContaining"
| "stringContaining"
| "stringMatching"
| "anything"
| "any"
| "closeTo"
) {
return None;
}
let mut negated = false;
if let Some(RegistrationCallee::Property { object: base, name }) =
registration_callee(object, source)
{
if name == "not" {
object = base;
negated = true;
}
}
let mut receiver = super::registrations::unwrap_registration_callee(object)?;
while matches!(
receiver.kind(),
"member_expression" | "subscript_expression"
) {
receiver = super::registrations::unwrap_registration_callee(
receiver.child_by_field_name("object")?,
)?;
}
(self.is_factory(object, source)
&& !self
.mutated_matcher_factories
.contains(node_text(receiver, source)))
.then_some((negated, matcher))
}
fn is_locally_shadowed(&self, node: Node<'_>, expected: &str) -> bool {
position_is_shadowed(&self.shadow_ranges, expected, node)
}
fn collect_import(
&mut self,
import: Node<'_>,
source: &[u8],
facade_modules: &BTreeSet<String>,
configured_modules: &BTreeSet<String>,
) -> BTreeSet<String> {
let mut trusted = BTreeSet::new();
if is_type_only_declaration(import) {
return trusted;
}
let Some(module) = import_statement_module(import, source) else {
return trusted;
};
let exact_assertion_module = ASSERTION_MODULES.contains(&module);
let declared_module = configured_modules.contains(module);
let framework_facade = facade_modules.contains(module);
if !exact_assertion_module && !framework_facade {
return trusted;
}
let mut stack = vec![import];
while let Some(node) = stack.pop() {
match node.kind() {
"import_specifier" if !is_type_only_specifier(node) => {
let Some(name) = node.child_by_field_name("name") else {
continue;
};
if node_text(name, source) == "expect"
|| (module == "expect" && node_text(name, source) == "default")
{
let local = node.child_by_field_name("alias").unwrap_or(name);
let local = node_text(local, source).to_owned();
self.identifiers.insert(local.clone());
trusted.insert(local);
}
}
"namespace_import" if exact_assertion_module || declared_module => {
if let Some(identifier) = node.named_child(0) {
let local = node_text(identifier, source).to_owned();
self.namespaces.insert(local.clone());
trusted.insert(local);
}
}
"import_clause" if module == "expect" => {
if let Some(identifier) = node
.named_child(0)
.filter(|child| child.kind() == "identifier")
{
let local = node_text(identifier, source).to_owned();
self.identifiers.insert(local.clone());
trusted.insert(local);
}
}
"import_require_clause" => {
if let Some(local) = node
.named_child(0)
.filter(|child| child.kind() == "identifier")
{
let local = node_text(local, source).to_owned();
if module == "expect" {
self.identifiers.insert(local.clone());
} else if exact_assertion_module || declared_module {
self.namespaces.insert(local.clone());
} else {
continue;
}
trusted.insert(local);
}
}
_ => {}
}
super::ast::push_named_children_reverse(node, &mut stack);
}
trusted
}
fn collect_require(
&mut self,
declaration: Node<'_>,
source: &[u8],
configured_modules: &BTreeSet<String>,
) -> BTreeSet<String> {
let mut trusted = BTreeSet::new();
let (Some(name), Some(value)) = (
declaration.child_by_field_name("name"),
declaration.child_by_field_name("value"),
) else {
return trusted;
};
let expect_property = (name.kind() == "identifier")
.then(|| registration_callee(value, source))
.flatten()
.and_then(|callee| match callee {
RegistrationCallee::Property { object, name } if name == "expect" => Some(object),
_ => None,
});
let member_expect = expect_property.is_some();
let required = expect_property.unwrap_or(value);
let Some(module) = required_module(required, source) else {
return trusted;
};
if !ASSERTION_MODULES.contains(&module) && !configured_modules.contains(module) {
return trusted;
}
match name.kind() {
"identifier" if module == "expect" || member_expect => {
let local = node_text(name, source).to_owned();
self.identifiers.insert(local.clone());
trusted.insert(local);
}
"identifier" => {
let local = node_text(name, source).to_owned();
self.namespaces.insert(local.clone());
trusted.insert(local);
}
"object_pattern" => {
let mut destructured = BTreeMap::new();
collect_expect_pattern(
name,
source,
&mut destructured,
Some(&mut trusted),
None,
false,
);
self.identifiers.extend(destructured.into_keys());
}
_ => {}
}
trusted
}
}
fn resolved_facade_modules(
root: Node<'_>,
source: &[u8],
registrations: &RegistrationNames,
) -> BTreeSet<String> {
let mut modules = BTreeSet::new();
let mut stack = vec![root];
while let Some(node) = stack.pop() {
if node.kind() == "import_statement"
&& import_has_runtime_bindings(node)
&& import_has_resolved_registration(node, source, registrations)
{
if let Some(module) = import_module(node, source)
.filter(|module| registrations.module_paths.contains_key(*module))
{
modules.insert(module.to_owned());
}
}
super::ast::push_named_children_reverse(node, &mut stack);
}
let paths: BTreeSet<_> = modules
.iter()
.filter_map(|module| registrations.module_paths.get(module))
.collect();
modules.extend(
registrations
.module_paths
.iter()
.filter_map(|(module, path)| paths.contains(path).then_some(module.clone())),
);
modules
}
fn import_has_resolved_registration(
import: Node<'_>,
source: &[u8],
registrations: &RegistrationNames,
) -> bool {
let mut stack = vec![import];
while let Some(node) = stack.pop() {
match node.kind() {
"import_specifier" if !is_type_only_specifier(node) => {
if node
.child_by_field_name("alias")
.or_else(|| node.child_by_field_name("name"))
.is_some_and(|local| registrations.recognizes_alias(node_text(local, source)))
{
return true;
}
continue;
}
"namespace_import"
if node.named_child(0).is_some_and(|local| {
registrations.recognizes_namespace(node_text(local, source))
}) =>
{
return true
}
_ => {}
}
super::ast::push_named_children_reverse(node, &mut stack);
}
false
}
fn extend_untrusted_bindings(
root: Node<'_>,
source: &[u8],
trusted: &BTreeSet<String>,
shadowed: &mut BTreeSet<String>,
) {
let mut locals = BTreeSet::new();
collect_binding_names(root, source, &mut locals);
shadowed.extend(locals.into_iter().filter(|local| !trusted.contains(local)));
}
pub(super) fn collect_binding_names(root: Node<'_>, source: &[u8], output: &mut BTreeSet<String>) {
let mut stack = vec![root];
while let Some(node) = stack.pop() {
match node.kind() {
"identifier"
| "type_identifier"
| "shorthand_property_identifier_pattern"
| "namespace_import" => {
output.insert(
node_text(node, source)
.trim_start_matches("* as ")
.to_owned(),
);
}
"import_specifier" => {
if !is_type_only_specifier(node) {
if let Some(local) = node
.child_by_field_name("alias")
.or_else(|| node.child_by_field_name("name"))
{
output.insert(node_text(local, source).to_owned());
}
}
continue;
}
"pair_pattern" => {
if let Some(value) = node.child_by_field_name("value") {
stack.push(value);
}
continue;
}
"assignment_pattern" | "object_assignment_pattern" => {
if let Some(left) = node
.child_by_field_name("left")
.or_else(|| node.named_child(0))
{
stack.push(left);
}
continue;
}
"required_parameter" | "optional_parameter" => {
if let Some(binding) = node
.child_by_field_name("name")
.or_else(|| node.child_by_field_name("pattern"))
{
stack.push(binding);
}
continue;
}
"type_annotation" => continue,
_ => {}
}
super::ast::push_named_children_reverse(node, &mut stack);
}
}
fn collect_assignment_targets(
root: Node<'_>,
source: &[u8],
output: &mut BTreeSet<String>,
factory_writes: &mut BTreeMap<String, usize>,
matcher_members: bool,
) {
let mut stack = vec![(root, 0usize)];
while let Some((node, depth)) = stack.pop() {
match node.kind() {
"identifier" | "shorthand_property_identifier_pattern" => {
output.insert(node_text(node, source).to_owned());
}
"member_expression" | "subscript_expression" => {
let property = node
.child_by_field_name("property")
.filter(|property| !node_text(*property, source).contains('\\'))
.map(|property| node_text(property, source).to_owned())
.or_else(|| {
node.child_by_field_name("index")
.and_then(|index| super::matcher::static_string_key(index, source))
});
if property.is_none_or(|property| {
property == "expect" || matcher_members && is_matcher_member(property.as_str())
}) {
if let Some(object) = node
.child_by_field_name("object")
.and_then(super::registrations::unwrap_registration_callee)
{
if matcher_members
&& matches!(object.kind(), "member_expression" | "subscript_expression")
{
stack.push((object, depth + 1));
} else if object.kind() == "identifier" {
let reached = depth + 1;
factory_writes
.entry(node_text(object, source).to_owned())
.and_modify(|deepest| *deepest = (*deepest).max(reached))
.or_insert(reached);
}
}
}
continue;
}
"pair" | "pair_pattern" => {
if let Some(value) = node.child_by_field_name("value") {
stack.push((value, depth));
}
continue;
}
"assignment_pattern" | "object_assignment_pattern" => {
if let Some(left) = node
.child_by_field_name("left")
.or_else(|| node.named_child(0))
{
stack.push((left, depth));
}
continue;
}
"type_annotation" => continue,
_ => {}
}
super::ast::push_named_children_reverse_scoped(node, depth, &mut stack);
}
}
fn assignment_target(node: Node<'_>) -> Option<Node<'_>> {
match node.kind() {
"assignment_expression" | "augmented_assignment_expression" => {
node.child_by_field_name("left")
}
"update_expression" => node.child_by_field_name("argument"),
"unary_expression"
if node
.child_by_field_name("operator")
.is_some_and(|op| op.kind() == "delete") =>
{
node.child_by_field_name("argument")
}
"for_in_statement" => node
.child_by_field_name("left")
.filter(|left| loop_binding_keyword(node, *left).is_none()),
_ => None,
}
}
fn module_runtime_binding_exists(
root: Node<'_>,
source: &[u8],
expected: &str,
shadow_ranges: &BTreeMap<String, Vec<(usize, usize)>>,
) -> bool {
let mut stack = vec![root];
while let Some(node) = stack.pop() {
let assignment = assignment_target(node);
let pattern = match node.kind() {
"import_statement" if import_has_runtime_bindings(node) => Some(node),
"variable_declarator" if is_top_level_variable(node) => {
node.child_by_field_name("name")
}
"variable_declarator" if is_module_var(node) => node.child_by_field_name("name"),
"for_in_statement"
if node.child_by_field_name("left").is_some_and(|left| {
loop_binding_keyword(node, left) == Some("var")
&& nearest_function_scope(node.parent()).is_none()
}) =>
{
node.child_by_field_name("left")
}
"function_declaration"
| "generator_function_declaration"
| "class_declaration"
| "abstract_class_declaration"
| "enum_declaration"
if is_module_declaration(node) =>
{
node.child_by_field_name("name")
}
"internal_module" | "module"
if !is_erased_declaration(node) && is_module_declaration(node) =>
{
node.child_by_field_name("name")
}
_ => assignment.filter(|left| {
matches!(
left.kind(),
"identifier" | "object_pattern" | "array_pattern"
) && !position_is_shadowed(shadow_ranges, expected, node)
}),
};
if let Some(pattern) = pattern {
let mut names = BTreeSet::new();
if assignment.is_some() {
collect_assignment_targets(
pattern,
source,
&mut names,
&mut BTreeMap::new(),
false,
);
} else {
collect_binding_names(pattern, source, &mut names);
}
if names.contains(expected) {
return true;
}
}
super::ast::push_named_children_reverse(node, &mut stack);
}
false
}
type BindingScopes = BTreeMap<usize, BTreeSet<String>>;
type ScopeRanges = BTreeMap<usize, (usize, usize)>;
type ScopeParents = BTreeMap<usize, usize>;
fn namespace_alias_writes(
root: Node<'_>,
source: &[u8],
scopes: &BindingScopes,
scope_parents: &ScopeParents,
matcher_members: bool,
require_shadowed: bool,
) -> BTreeSet<String> {
let key = |name: &str, mut scope: usize| {
while scope != root.id() {
if scopes.get(&scope).is_some_and(|names| names.contains(name)) {
return (scope, name.to_owned());
}
scope = scope_parents[&scope];
}
(root.id(), name.to_owned())
};
let mut required_aliases = BTreeMap::new();
let mut aliases = BTreeMap::<_, BTreeSet<(_, usize, bool)>>::new();
let mut writes = BTreeMap::<_, usize>::new();
let mut stack = vec![(root, root.id())];
while let Some((node, scope)) = stack.pop() {
let edge = match node.kind() {
"variable_declarator" => node
.child_by_field_name("name")
.zip(node.child_by_field_name("value")),
"assignment_pattern" | "object_assignment_pattern" => node
.child_by_field_name("left")
.zip(node.child_by_field_name("right")),
"required_parameter" | "optional_parameter" => node
.child_by_field_name("name")
.or_else(|| node.child_by_field_name("pattern"))
.zip(node.child_by_field_name("value")),
"assignment_expression" => node
.child_by_field_name("left")
.zip(node.child_by_field_name("right")),
"augmented_assignment_expression"
if node
.child_by_field_name("operator")
.is_some_and(|operator| matches!(operator.kind(), "||=" | "&&=" | "??=")) =>
{
node.child_by_field_name("left")
.zip(node.child_by_field_name("right"))
}
_ => None,
};
if let Some((left, right)) = edge {
if let Some(right) = super::registrations::unwrap_registration_callee(right) {
let mut locals = BTreeMap::new();
let mut shallow_locals = BTreeMap::new();
let mut owners = BTreeMap::new();
if left.kind() == "identifier" {
locals.insert(node_text(left, source).to_owned(), 0);
} else if matcher_members && left.kind() == "object_pattern" {
collect_expect_pattern(
left,
source,
&mut locals,
None,
Some(&mut shallow_locals),
true,
);
}
let required = match registration_callee(right, source) {
Some(RegistrationCallee::Property { object, name }) if name == "expect" => {
object
}
_ => right,
};
if !require_shadowed && key("require", scope).0 == root.id() {
if let Some(module) = required_module(required, source)
.filter(|module| ASSERTION_MODULES.contains(module))
{
for (local, copied) in locals
.keys()
.map(|local| (local, false))
.chain(shallow_locals.keys().map(|local| (local, true)))
{
let local = key(local, scope);
let owner = required_aliases
.entry(module)
.or_insert_with(|| local.clone());
aliases.entry(owner.clone()).or_default().insert((
local.clone(),
0,
false,
));
aliases
.entry(local)
.or_default()
.insert((owner.clone(), 0, copied));
}
}
}
if right.kind() == "identifier" {
owners.insert(node_text(right, source).to_owned(), 0);
} else if matcher_members
&& matches!(right.kind(), "member_expression" | "subscript_expression")
{
let mut owner_writes = BTreeMap::new();
collect_assignment_targets(
right,
source,
&mut BTreeSet::new(),
&mut owner_writes,
true,
);
owners.extend(owner_writes);
}
for (local, local_delta, copied) in locals
.iter()
.map(|(local, delta)| (local, delta, false))
.chain(
shallow_locals
.iter()
.map(|(local, delta)| (local, delta, true)),
)
{
for (owner, owner_delta) in &owners {
aliases.entry(key(local, scope)).or_default().insert((
key(owner, scope),
local_delta + owner_delta,
copied,
));
}
}
}
}
if let Some(target) = assignment_target(node) {
let mut receivers = BTreeMap::new();
collect_assignment_targets(
target,
source,
&mut BTreeSet::new(),
&mut receivers,
matcher_members,
);
for (name, depth) in receivers {
writes
.entry(key(&name, scope))
.and_modify(|deepest| *deepest = (*deepest).max(depth))
.or_insert(depth);
}
}
let scope = if is_local_scope(node) {
node.id()
} else {
scope
};
super::ast::push_named_children_reverse_scoped(node, scope, &mut stack);
}
let mut deepest = BTreeMap::<_, usize>::new();
let mut modules = BTreeSet::new();
let mut pending: Vec<_> = writes
.into_iter()
.map(|(binding, depth)| (binding, depth.min(COPY_CROSS_DEPTH)))
.collect();
while let Some((binding, depth)) = pending.pop() {
if deepest
.get(&binding)
.is_some_and(|reached| *reached >= depth)
{
continue;
}
deepest.insert(binding.clone(), depth);
if binding.0 == root.id() {
modules.insert(binding.1.clone());
}
if let Some(sources) = aliases.get(&binding) {
for (source, delta, copied) in sources {
if *copied && depth < COPY_CROSS_DEPTH {
continue;
}
pending.push((source.clone(), (depth + delta).min(COPY_CROSS_DEPTH)));
}
}
}
modules
}
pub(super) fn collect_binding_scopes(
root: Node<'_>,
source: &[u8],
) -> (BindingScopes, ScopeRanges, ScopeParents) {
let mut scopes = BTreeMap::<usize, BTreeSet<String>>::new();
let mut scope_ranges = BTreeMap::new();
let mut scope_parents = BTreeMap::new();
let mut stack = vec![(root, root.id())];
while let Some((node, enclosing)) = stack.pop() {
let scope = if is_local_scope(node) {
scope_ranges.insert(node.id(), (node.start_byte(), node.end_byte()));
scope_parents.insert(node.id(), enclosing);
node.id()
} else {
enclosing
};
match node.kind() {
"ambient_declaration" => continue,
"function_declaration"
| "generator_function_declaration"
| "function_expression"
| "generator_function"
| "arrow_function"
| "method_definition" => {
if let Some(name) = node.child_by_field_name("name") {
if node.kind() != "method_definition" {
add_scope_binding(&mut scopes, node, name, source);
}
if matches!(
node.kind(),
"function_declaration" | "generator_function_declaration"
) {
if let Some(scope) = nearest_lexical_scope(node.parent()) {
add_scope_binding(&mut scopes, scope, name, source);
}
}
}
if let Some(parameters) = node
.child_by_field_name("parameters")
.or_else(|| node.child_by_field_name("parameter"))
{
add_scope_binding(&mut scopes, node, parameters, source);
}
}
"class_declaration" | "abstract_class_declaration" | "class" => {
if let Some(name) = node.child_by_field_name("name") {
add_scope_binding(&mut scopes, node, name, source);
if node.kind() != "class" {
if let Some(scope) = nearest_lexical_scope(node.parent()) {
add_scope_binding(&mut scopes, scope, name, source);
}
}
}
}
"catch_clause" => {
if let Some(parameter) = node.child_by_field_name("parameter") {
add_scope_binding(&mut scopes, node, parameter, source);
}
}
"for_statement" | "for_in_statement" => {
if let Some(binding) = node
.child_by_field_name("initializer")
.or_else(|| node.child_by_field_name("left"))
{
let keyword = match binding.kind() {
"variable_declaration" => Some("var"),
"lexical_declaration" => Some("let"),
_ => loop_binding_keyword(node, binding),
};
let target = if keyword == Some("var") {
nearest_function_scope(node.parent())
} else if matches!(keyword, Some("let" | "const" | "using")) {
Some(node)
} else {
None
};
if let Some(target) = target {
if !matches!(
binding.kind(),
"lexical_declaration" | "variable_declaration"
) {
add_scope_binding(&mut scopes, target, binding, source);
}
}
}
}
"variable_declarator" if !is_top_level_variable(node) => {
if let (Some(declaration), Some(name)) =
(node.parent(), node.child_by_field_name("name"))
{
let scope = if declaration.kind() == "variable_declaration" {
nearest_function_scope(declaration.parent())
} else {
nearest_lexical_scope(declaration.parent())
};
if let Some(scope) = scope {
add_scope_binding(&mut scopes, scope, name, source);
}
}
}
"enum_declaration" if !is_module_declaration(node) => {
if let (Some(scope), Some(name)) = (
nearest_lexical_scope(node.parent()),
node.child_by_field_name("name"),
) {
add_scope_binding(&mut scopes, scope, name, source);
}
}
"internal_module" | "module"
if !is_erased_declaration(node) && !is_module_declaration(node) =>
{
if let (Some(scope), Some(name)) = (
nearest_lexical_scope(node.parent()),
node.child_by_field_name("name"),
) {
add_scope_binding(&mut scopes, scope, name, source);
}
}
_ => {}
}
super::ast::push_named_children_reverse_scoped(node, scope, &mut stack);
}
(scopes, scope_ranges, scope_parents)
}
fn collect_scoped_bindings(
scopes: BindingScopes,
scope_ranges: ScopeRanges,
) -> BTreeMap<String, Vec<(usize, usize)>> {
let mut ranges = BTreeMap::<String, Vec<(usize, usize)>>::new();
for (scope, names) in scopes {
let Some(range) = scope_ranges.get(&scope).copied() else {
continue;
};
for name in names {
ranges.entry(name).or_default().push(range);
}
}
for entries in ranges.values_mut() {
entries.sort_unstable();
let mut merged = Vec::<(usize, usize)>::with_capacity(entries.len());
for &(start, end) in entries.iter() {
if let Some(previous) = merged.last_mut().filter(|previous| start <= previous.1) {
previous.1 = previous.1.max(end);
} else {
merged.push((start, end));
}
}
*entries = merged;
}
ranges
}
fn position_is_shadowed(
shadow_ranges: &BTreeMap<String, Vec<(usize, usize)>>,
expected: &str,
node: Node<'_>,
) -> bool {
let Some(ranges) = shadow_ranges.get(expected) else {
return false;
};
let index = ranges.partition_point(|(start, _)| *start <= node.start_byte());
index > 0 && ranges[index - 1].1 >= node.end_byte()
}
fn add_scope_binding(
scopes: &mut BTreeMap<usize, BTreeSet<String>>,
scope: Node<'_>,
pattern: Node<'_>,
source: &[u8],
) {
collect_binding_names(pattern, source, scopes.entry(scope.id()).or_default());
}
pub(super) fn loop_binding_keyword(loop_node: Node<'_>, binding: Node<'_>) -> Option<&'static str> {
(0..loop_node.child_count()).find_map(|index| {
let child = loop_node.child(index)?;
(child.end_byte() <= binding.start_byte()).then(|| match child.kind() {
"const" => Some("const"),
"let" => Some("let"),
"using" => Some("using"),
"var" => Some("var"),
_ => None,
})?
})
}
fn is_module_var(declarator: Node<'_>) -> bool {
declarator
.parent()
.is_some_and(|declaration| declaration.kind() == "variable_declaration")
&& !is_erased_declaration(declarator)
&& nearest_function_scope(declarator.parent()).is_none()
}
pub(super) fn is_local_scope(node: Node<'_>) -> bool {
matches!(
node.kind(),
"function_declaration"
| "generator_function_declaration"
| "function_expression"
| "generator_function"
| "arrow_function"
| "method_definition"
| "class_declaration"
| "abstract_class_declaration"
| "class"
| "class_static_block"
| "internal_module"
| "module"
| "statement_block"
| "for_statement"
| "for_in_statement"
| "switch_body"
| "catch_clause"
)
}
pub(super) fn nearest_function_scope(mut node: Option<Node<'_>>) -> Option<Node<'_>> {
while let Some(candidate) = node {
if matches!(
candidate.kind(),
"function_declaration"
| "generator_function_declaration"
| "function_expression"
| "generator_function"
| "arrow_function"
| "method_definition"
| "class_static_block"
| "internal_module"
| "module"
) {
return Some(candidate);
}
if candidate.kind() == "program" {
return None;
}
node = candidate.parent();
}
None
}
fn nearest_lexical_scope(mut node: Option<Node<'_>>) -> Option<Node<'_>> {
while let Some(candidate) = node {
if matches!(
candidate.kind(),
"statement_block"
| "for_statement"
| "for_in_statement"
| "switch_body"
| "catch_clause"
| "class_static_block"
| "internal_module"
| "module"
) {
return Some(candidate);
}
if candidate.kind() == "program" {
return None;
}
node = candidate.parent();
}
None
}
fn is_module_declaration(node: Node<'_>) -> bool {
let mut parent = node.parent();
while let Some(candidate) = parent {
match candidate.kind() {
"program" => return true,
"expression_statement" | "export_statement" => parent = candidate.parent(),
_ => return false,
}
}
false
}
pub(super) fn is_erased_declaration(mut node: Node<'_>) -> bool {
loop {
if node.kind() == "ambient_declaration" {
return true;
}
if node.kind() == "program" {
return false;
}
let Some(parent) = node.parent() else {
return false;
};
node = parent;
}
}
fn required_module<'a>(call: Node<'_>, source: &'a [u8]) -> Option<&'a str> {
if call.kind() != "call_expression" {
return None;
}
let function = call.child_by_field_name("function")?;
if function.kind() != "identifier" || node_text(function, source) != "require" {
return None;
}
let arguments = call.child_by_field_name("arguments")?;
string_literal(arguments.named_child(0)?, source)
}
fn is_matcher_member(name: &str) -> bool {
matches!(
name,
"not"
| "objectContaining"
| "arrayContaining"
| "stringContaining"
| "stringMatching"
| "anything"
| "any"
| "closeTo"
)
}
fn shorthand_key_name(node: Node<'_>, source: &[u8]) -> Option<String> {
let text = node_text(node, source);
(!text.contains('\\')).then(|| text.to_owned())
}
fn record_matcher_local(
identifiers: &mut BTreeMap<String, usize>,
trusted: &mut Option<&mut BTreeSet<String>>,
local: String,
depth: usize,
known_expect: bool,
top_level: bool,
) {
identifiers.insert(local.clone(), depth + 1);
if known_expect && top_level {
if let Some(trusted) = trusted.as_deref_mut() {
trusted.insert(local);
}
}
}
fn collect_expect_pattern(
pattern: Node<'_>,
source: &[u8],
identifiers: &mut BTreeMap<String, usize>,
mut trusted: Option<&mut BTreeSet<String>>,
mut shallow: Option<&mut BTreeMap<String, usize>>,
include_defaults: bool,
) {
let mut pending = vec![(pattern, 0usize)];
while let Some((pattern, depth)) = pending.pop() {
let top_level = depth == 0;
let mut cursor = pattern.walk();
for property in pattern.named_children(&mut cursor) {
match property.kind() {
"object_assignment_pattern" if include_defaults => {
if let Some(left) = property.child_by_field_name("left") {
let name = shorthand_key_name(left, source);
if name
.as_deref()
.is_none_or(|name| name == "expect" || is_matcher_member(name))
{
identifiers.insert(node_text(left, source).to_owned(), depth + 1);
}
}
}
"rest_pattern" if include_defaults => {
if let Some(binding) = property.named_child(0) {
if binding.kind() == "identifier" {
if let Some(shallow) = shallow.as_deref_mut() {
shallow.insert(node_text(binding, source).to_owned(), depth);
}
}
}
}
"shorthand_property_identifier_pattern" => {
let name = shorthand_key_name(property, source);
let known_expect = name.as_deref() == Some("expect");
let known_not = name.as_deref().is_some_and(is_matcher_member);
if known_expect || (include_defaults && (known_not || name.is_none())) {
let local = node_text(property, source).to_owned();
record_matcher_local(
identifiers,
&mut trusted,
local,
depth,
known_expect,
top_level,
);
}
}
"pair_pattern" => {
let (Some(key), Some(value)) = (
property.child_by_field_name("key"),
property.child_by_field_name("value"),
) else {
continue;
};
let computed = key.kind() == "computed_property_name";
let key = if computed {
key.named_child(0).unwrap_or(key)
} else {
key
};
let value = if include_defaults
&& matches!(
value.kind(),
"assignment_pattern" | "object_assignment_pattern"
) {
value.child_by_field_name("left").unwrap_or(value)
} else {
value
};
let text = node_text(key, source);
let name = if key.kind() == "string" {
super::matcher::static_string_key(key, source)
} else if computed || text.contains('\\') {
None
} else {
Some(text.to_owned())
};
let known_expect = name.as_deref() == Some("expect");
let known_not = name.as_deref().is_some_and(is_matcher_member);
let supported =
known_expect || (include_defaults && (known_not || name.is_none()));
if supported && value.kind() == "identifier" {
let local = node_text(value, source).to_owned();
record_matcher_local(
identifiers,
&mut trusted,
local,
depth,
known_expect,
top_level,
);
} else if supported && include_defaults && value.kind() == "object_pattern" {
pending.push((value, depth + 1));
}
}
_ => {}
}
}
}
}