Files
millionco__react-doctor/native/oxlint/rules/window-open-without-noopener.rs

5756 lines
223 KiB
Rust

use std::{
cell::{Cell, RefCell},
path::{Path, PathBuf},
};
use oxc_allocator::Allocator;
use oxc_ast::{
AstKind,
ast::{
Argument, ArrayExpressionElement, ArrowFunctionBody, BindingPattern, CallExpression,
ExportDefaultDeclarationKind, Expression, JSXAttributeValue, JSXElementName,
ObjectPropertyKind, TSType, TSTypeName,
},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_parser::Parser;
use oxc_resolver::{ResolveOptions, Resolver, TsconfigDiscovery};
use oxc_semantic::{NodeId, Semantic, SemanticBuilder, SymbolId};
use oxc_span::{GetSpan, SourceType, Span};
use oxc_syntax::operator::{AssignmentOperator, LogicalOperator, UnaryOperator};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::{
AstNode,
context::LintContext,
module_record::{ExportExportName, ExportImportName, ImportImportName, ModuleRecord},
rule::Rule,
};
const MESSAGE: &str = "This `window.open` call leaves the opened page able to redirect your tab via `window.opener`, so pass `'noopener'` in the features argument.";
const MAX_RESOLUTION_DEPTH: usize = 8;
const MAX_CROSS_FILE_RESOLUTIONS: usize = 3;
const MAX_CROSS_FILE_REEXPORT_DEPTH: usize = 4;
const MAX_CROSS_FILE_BYTES: u64 = 2_000_000;
const MAX_TSCONFIG_DIRECTORY_WALK: usize = 30;
const MAX_TSCONFIG_EXTENDS_DEPTH: usize = 8;
const OPAQUE_FEATURE_TEXT: char = '\0';
#[derive(Debug, Default, Clone)]
pub struct WindowOpenWithoutNoopener;
declare_oxc_lint!(
/// Require opener protection for discarded dynamic window handles.
WindowOpenWithoutNoopener,
react_doctor_native,
correctness,
version = "0.1.0",
short_description = "Require noopener for discarded dynamic window handles.",
);
impl Rule for WindowOpenWithoutNoopener {
fn run_once<'a>(&self, ctx: &LintContext<'a>) {
let mut analysis = WindowOpenAnalysis::new(ctx);
for node in ctx.nodes().iter() {
let AstKind::CallExpression(call) = node.kind() else {
continue;
};
if !analysis.is_window_open_call(node, call)
|| !analysis.is_discarded_handle(node, &mut FxHashSet::default())
{
continue;
}
analysis.current_open = Some(node.id());
let destination_argument = call.arguments.first();
let destination = destination_argument.and_then(Argument::as_expression);
let is_direct_external_literal = destination.is_some_and(|expression| {
let Expression::StringLiteral(literal) = expression else {
return false;
};
!literal
.value
.trim_matches(is_ecmascript_whitespace)
.is_empty()
&& !WindowOpenAnalysis::is_trusted_foreign_static_text(literal.value.as_str())
});
let is_trusted_destination = match destination_argument {
None => true,
Some(argument) => argument.as_expression().is_some_and(|expression| {
analysis.is_trusted_or_nullish(Some(expression), 0, &mut FxHashSet::default())
}),
};
if !is_direct_external_literal && is_trusted_destination {
continue;
}
if call
.arguments
.get(1)
.and_then(Argument::as_expression)
.and_then(|expression| match expression {
Expression::StringLiteral(literal) => Some(literal.value.as_str()),
_ => None,
})
.is_some_and(|target| matches!(target, "_self" | "_top" | "_parent"))
{
continue;
}
if let Some(features_argument) = call.arguments.get(2) {
let Some(features) = features_argument.as_expression() else {
continue;
};
if !analysis.is_raw_nullish(features) {
let mut visited_symbols = FxHashSet::default();
let Some(features_text) =
analysis.resolve_static_string(features, 0, &mut visited_symbols)
else {
continue;
};
if WindowOpenAnalysis::features_may_protect_opener(&features_text) {
continue;
}
}
}
ctx.diagnostic(OxcDiagnostic::warn(MESSAGE).with_label(call.span));
}
}
}
struct WindowOpenAnalysis<'a, 'ctx> {
ctx: &'ctx LintContext<'a>,
nodes_by_span: FxHashMap<(u32, u32), NodeId>,
current_open: Option<NodeId>,
property_write_analysis: PossibleStaticPropertyWriteAnalysis,
cross_file_remaining: Cell<usize>,
cross_file_seen: RefCell<FxHashSet<(String, String)>>,
cross_file_memo: RefCell<FxHashMap<(String, String, bool), Option<bool>>>,
local_serialization_reference: Cell<Option<NodeId>>,
}
impl<'a, 'ctx> WindowOpenAnalysis<'a, 'ctx> {
fn new(ctx: &'ctx LintContext<'a>) -> Self {
let mut nodes_by_span = FxHashMap::default();
for node in ctx.nodes().iter() {
nodes_by_span.insert((node.span().start, node.span().end), node.id());
}
Self {
ctx,
nodes_by_span,
current_open: None,
property_write_analysis: build_possible_static_property_write_analysis(ctx),
cross_file_remaining: Cell::new(MAX_CROSS_FILE_RESOLUTIONS),
cross_file_seen: RefCell::new(FxHashSet::default()),
cross_file_memo: RefCell::new(FxHashMap::default()),
local_serialization_reference: Cell::new(None),
}
}
fn expression_node(&self, expression: &Expression<'a>) -> Option<&AstNode<'a>> {
self.node_for_span(expression.span())
}
fn node_for_span(&self, span: Span) -> Option<&AstNode<'a>> {
self.nodes_by_span
.get(&(span.start, span.end))
.map(|node_id| self.ctx.nodes().get_node(*node_id))
}
fn is_window_open_call(&self, _node: &AstNode<'a>, call: &CallExpression<'a>) -> bool {
self.is_global_open_reference(&call.callee, 0, &mut FxHashSet::default())
}
fn is_global_open_reference(
&self,
expression: &Expression<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
if depth > MAX_RESOLUTION_DEPTH {
return false;
}
if is_proven_global_namespace_reference(expression, "open", self.ctx) {
return true;
}
if let Some(member) = expression.get_inner_expression().as_member_expression()
&& member.static_property_name() == Some("open")
&& ["frames", "globalThis", "parent", "self", "top", "window"]
.iter()
.any(|name| is_proven_global_namespace_reference(member.object(), name, self.ctx))
{
return true;
}
let Expression::Identifier(identifier) = expression.get_inner_expression() else {
return false;
};
let Some(symbol_id) = self
.ctx
.scoping()
.get_reference(identifier.reference_id())
.symbol_id()
else {
return false;
};
if !visited_symbols.insert(symbol_id) {
return false;
}
self.const_initializer(symbol_id)
.is_some_and(|initializer| {
self.is_global_open_reference(initializer, depth + 1, visited_symbols)
})
}
fn is_discarded_handle(
&self,
node: &AstNode<'a>,
visited_functions: &mut FxHashSet<NodeId>,
) -> bool {
let root = self.transparent_node_root(node);
let parent = self.ctx.nodes().parent_node(root.id());
match parent.kind() {
AstKind::ExpressionStatement(_) => true,
AstKind::UnaryExpression(unary) => unary.operator == UnaryOperator::Void,
AstKind::AwaitExpression(_) => self.is_discarded_handle(parent, visited_functions),
AstKind::ReturnStatement(statement)
if statement
.argument
.as_ref()
.is_some_and(|argument| argument.span() == root.span()) =>
{
self.is_returned_handle_discarded(parent, visited_functions)
}
AstKind::LogicalExpression(expression) if expression.right.span() == root.span() => {
self.is_discarded_handle(parent, visited_functions)
}
AstKind::ConditionalExpression(expression)
if expression.consequent.span() == root.span()
|| expression.alternate.span() == root.span() =>
{
self.is_discarded_handle(parent, visited_functions)
}
AstKind::SequenceExpression(sequence) => {
sequence
.expressions
.last()
.is_none_or(|expression| expression.span() != root.span())
|| self.is_discarded_handle(parent, visited_functions)
}
AstKind::ArrowFunctionExpression(arrow)
if !matches!(&arrow.body, ArrowFunctionBody::FunctionBody(_))
&& arrow.body.span() == root.span() =>
{
self.is_arrow_return_discarded(parent, visited_functions)
}
_ => false,
}
}
fn is_returned_handle_discarded(
&self,
return_node: &AstNode<'a>,
visited_functions: &mut FxHashSet<NodeId>,
) -> bool {
let Some(function) = self.enclosing_function(return_node) else {
return false;
};
self.is_arrow_return_discarded(function, visited_functions)
}
fn is_arrow_return_discarded(
&self,
function_node: &AstNode<'a>,
visited_functions: &mut FxHashSet<NodeId>,
) -> bool {
if !visited_functions.insert(function_node.id()) {
return false;
}
let parent = self.ctx.nodes().parent_node(function_node.id());
match parent.kind() {
AstKind::JSXExpressionContainer(_) => {
let attribute_node = self.ctx.nodes().parent_node(parent.id());
let opening_node = self.ctx.nodes().parent_node(attribute_node.id());
matches!(attribute_node.kind(), AstKind::JSXAttribute(attribute)
if Self::jsx_attribute_name(attribute).is_some_and(Self::is_event_handler_name))
&& matches!(opening_node.kind(), AstKind::JSXOpeningElement(opening)
if matches!(&opening.name, JSXElementName::Identifier(identifier)
if identifier.name.as_bytes().first().is_some_and(u8::is_ascii_lowercase)))
}
AstKind::ExpressionStatement(_) => true,
AstKind::CallExpression(call) => {
if call.callee.span() == function_node.span() {
return self.is_discarded_handle(parent, visited_functions);
}
call.arguments
.iter()
.any(|argument| argument.span() == function_node.span())
&& self.call_discards_callback_return(parent, call)
}
AstKind::ObjectProperty(property)
if property.value.span() == function_node.span()
&& !property.computed
&& property
.key
.static_name()
.is_some_and(|name| Self::is_event_handler_name(name.as_ref())) =>
{
self.is_intrinsic_create_element_props_object(parent)
}
_ => self
.direct_local_function_calls(function_node)
.is_some_and(|calls| {
calls.is_empty()
|| calls.iter().all(|call_id| {
self.is_discarded_handle(
self.ctx.nodes().get_node(*call_id),
visited_functions,
)
})
}),
}
}
fn call_discards_callback_return(
&self,
call_node: &AstNode<'a>,
call: &CallExpression<'a>,
) -> bool {
match call.callee.get_inner_expression() {
Expression::Identifier(identifier) => {
matches!(
identifier.name.as_str(),
"queueMicrotask"
| "requestAnimationFrame"
| "setInterval"
| "setTimeout"
| "addEventListener"
) && is_proven_global_namespace_reference(
&call.callee,
identifier.name.as_str(),
self.ctx,
)
}
expression => {
let Some(member) = expression.as_member_expression() else {
return false;
};
match member.static_property_name() {
Some("addEventListener") => {
is_proven_global_namespace_reference(member.object(), "window", self.ctx)
}
Some("forEach") => self.is_proven_array_receiver(member.object(), call_node),
_ => false,
}
}
}
}
fn is_proven_array_receiver(&self, receiver: &Expression<'a>, reference: &AstNode<'a>) -> bool {
match receiver.get_inner_expression() {
Expression::ArrayExpression(_) => true,
Expression::Identifier(identifier) => {
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if self.symbol_has_write_before(symbol_id, reference) {
return false;
}
if has_possible_static_property_write_before(
identifier,
"forEach",
reference,
&self.property_write_analysis,
self.ctx,
) {
return false;
}
if self.binding_has_builtin_array_annotation(symbol_id) {
return true;
}
self.const_initializer(symbol_id).is_some_and(|initializer| {
matches!(initializer.get_inner_expression(), Expression::ArrayExpression(_))
|| matches!(initializer.get_inner_expression(), Expression::NewExpression(new_expression)
if is_proven_global_namespace_reference(&new_expression.callee, "Array", self.ctx)
&& !self.global_namespace_was_mutated_before("Array", reference))
|| matches!(initializer.get_inner_expression(), Expression::CallExpression(call)
if call.callee.get_inner_expression().as_member_expression().is_some_and(|member|
matches!(member.static_property_name(), Some("from" | "of"))
&& is_proven_global_namespace_reference(member.object(), "Array", self.ctx)
&& !self.global_namespace_was_mutated_before("Array", reference)
&& !self.global_property_was_mutated_before(
"Array",
member.static_property_name().unwrap_or_default(),
reference,
)))
})
}
_ => false,
}
}
fn binding_has_builtin_array_annotation(&self, symbol_id: SymbolId) -> bool {
let declaration = self.ctx.symbol_declaration(symbol_id);
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return false;
};
declarator
.type_annotation
.as_ref()
.is_some_and(|annotation| {
matches!(
&annotation.type_annotation,
TSType::TSArrayType(_) | TSType::TSTupleType(_)
) || matches!(&annotation.type_annotation, TSType::TSTypeReference(reference)
if matches!(&reference.type_name, TSTypeName::IdentifierReference(identifier)
if matches!(identifier.name.as_str(), "Array" | "ReadonlyArray")))
})
}
fn is_intrinsic_create_element_props_object(&self, property_node: &AstNode<'a>) -> bool {
let object_node = self.ctx.nodes().parent_node(property_node.id());
let call_node = self.ctx.nodes().parent_node(object_node.id());
let AstKind::ObjectExpression(_) = object_node.kind() else {
return false;
};
let AstKind::CallExpression(call) = call_node.kind() else {
return false;
};
if call
.arguments
.get(1)
.is_none_or(|argument| argument.span() != object_node.span())
|| !matches!(
call.arguments.first().and_then(Argument::as_expression),
Some(Expression::StringLiteral(_))
)
{
return false;
}
let callee = call.callee.get_inner_expression();
self.is_react_api_call(call, "createElement")
|| callee.as_member_expression().is_some_and(|member| {
member.static_property_name() == Some("createElement")
&& is_proven_global_namespace_reference(member.object(), "React", self.ctx)
})
}
fn transparent_node_root(&self, mut node: &'ctx AstNode<'a>) -> &'ctx AstNode<'a> {
loop {
let parent = self.ctx.nodes().parent_node(node.id());
let is_transparent = matches!(
parent.kind(),
AstKind::ParenthesizedExpression(_)
| AstKind::TSAsExpression(_)
| AstKind::TSSatisfiesExpression(_)
| AstKind::TSNonNullExpression(_)
| AstKind::ChainExpression(_)
);
if !is_transparent || !parent.span().contains_inclusive(node.span()) {
return node;
}
node = parent;
}
}
fn enclosing_function(&self, node: &AstNode<'a>) -> Option<&AstNode<'a>> {
self.ctx.nodes().ancestors(node.id()).find(|ancestor| {
matches!(
ancestor.kind(),
AstKind::Function(_) | AstKind::ArrowFunctionExpression(_)
)
})
}
fn direct_local_function_calls(&self, function_node: &AstNode<'a>) -> Option<Vec<NodeId>> {
let Some(symbol_id) = self.local_function_symbol(function_node) else {
return None;
};
let aliases = self.const_alias_symbols(symbol_id);
let mut calls = Vec::new();
for alias in aliases {
for reference in self.ctx.scoping().get_resolved_references(alias) {
let reference_node = self.ctx.nodes().get_node(reference.node_id());
let root = self.transparent_node_root(reference_node);
let parent = self.ctx.nodes().parent_node(root.id());
if matches!(parent.kind(), AstKind::VariableDeclarator(declarator)
if declarator.init.as_ref().is_some_and(|init| init.span() == root.span()))
{
continue;
}
if !matches!(parent.kind(), AstKind::CallExpression(call)
if call.callee.span() == root.span())
{
return Some(Vec::new());
}
calls.push(parent.id());
}
}
(!calls.is_empty()).then_some(calls)
}
fn direct_named_local_function_calls(
&self,
function_node: &AstNode<'a>,
) -> Option<Vec<NodeId>> {
let symbol_id = self.local_function_symbol(function_node)?;
let mut calls = Vec::new();
for reference in self.ctx.scoping().get_resolved_references(symbol_id) {
let reference_node = self.ctx.nodes().get_node(reference.node_id());
let root = self.transparent_node_root(reference_node);
let parent = self.ctx.nodes().parent_node(root.id());
let AstKind::CallExpression(call) = parent.kind() else {
return None;
};
if call.callee.span() != root.span() {
return None;
}
calls.push(parent.id());
}
(!calls.is_empty()).then_some(calls)
}
fn local_function_symbol(&self, function_node: &AstNode<'a>) -> Option<SymbolId> {
match function_node.kind() {
AstKind::Function(function) => {
if self.is_exported_node(function_node) {
None
} else {
function
.id
.as_ref()
.map(|identifier| identifier.symbol_id())
}
}
AstKind::ArrowFunctionExpression(_) => {
let mut parent = self.ctx.nodes().parent_node(function_node.id());
if let AstKind::CallExpression(call) = parent.kind()
&& call
.arguments
.first()
.is_some_and(|argument| argument.span() == function_node.span())
&& Self::terminal_callee_name(&call.callee) == Some("useCallback")
{
parent = self.ctx.nodes().parent_node(parent.id());
}
let AstKind::VariableDeclarator(declarator) = parent.kind() else {
return None;
};
if !matches!(self.ctx.nodes().parent_node(parent.id()).kind(), AstKind::VariableDeclaration(declaration) if declaration.kind.is_const())
|| self.is_exported_node(self.ctx.nodes().parent_node(parent.id()))
{
return None;
}
declarator
.id
.get_binding_identifier()
.map(|identifier| identifier.symbol_id())
}
_ => None,
}
}
fn is_exported_node(&self, node: &AstNode<'a>) -> bool {
matches!(
self.ctx.nodes().parent_node(node.id()).kind(),
AstKind::ExportNamedDeclaration(_) | AstKind::ExportDefaultDeclaration(_)
)
}
fn const_alias_symbols(&self, root_symbol_id: SymbolId) -> Vec<SymbolId> {
let mut symbols = vec![root_symbol_id];
let mut seen = FxHashSet::from_iter([root_symbol_id]);
let mut index = 0;
while index < symbols.len() {
let source_symbol = symbols[index];
index += 1;
for reference in self.ctx.scoping().get_resolved_references(source_symbol) {
let reference_node = self.ctx.nodes().get_node(reference.node_id());
let root = self.transparent_node_root(reference_node);
let parent = self.ctx.nodes().parent_node(root.id());
let AstKind::VariableDeclarator(declarator) = parent.kind() else {
continue;
};
if !matches!(self.ctx.nodes().parent_node(parent.id()).kind(), AstKind::VariableDeclaration(declaration) if declaration.kind.is_const())
|| declarator
.init
.as_ref()
.is_none_or(|init| init.span() != root.span())
{
continue;
}
if let Some(alias) = declarator.id.get_binding_identifier()
&& seen.insert(alias.symbol_id())
{
symbols.push(alias.symbol_id());
}
}
}
symbols
}
fn is_trusted_or_nullish(
&self,
expression: Option<&Expression<'a>>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
self.is_nullish(expression)
|| expression.is_some_and(|expression| {
self.is_trusted_destination(expression, depth, visited_symbols)
})
}
fn is_trusted_destination(
&self,
expression: &Expression<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
if depth > MAX_RESOLUTION_DEPTH {
return false;
}
let expression = expression.get_inner_expression();
if self.is_trusted_static_destination(expression) {
return true;
}
match expression {
Expression::ConditionalExpression(conditional) => {
self.is_trusted_or_nullish(
Some(&conditional.consequent),
depth + 1,
&mut visited_symbols.clone(),
) && self.is_trusted_or_nullish(
Some(&conditional.alternate),
depth + 1,
&mut visited_symbols.clone(),
)
}
Expression::LogicalExpression(logical) => {
if logical.operator == LogicalOperator::And {
return self.is_nullish(Some(&logical.left))
|| self.is_trusted_or_nullish(
Some(&logical.right),
depth + 1,
visited_symbols,
);
}
if self.is_statically_truthy_trusted(&logical.left, depth + 1, visited_symbols) {
return true;
}
self.is_trusted_or_nullish(
Some(&logical.left),
depth + 1,
&mut visited_symbols.clone(),
) && self.is_trusted_or_nullish(
Some(&logical.right),
depth + 1,
&mut visited_symbols.clone(),
)
}
Expression::BinaryExpression(binary)
if binary.operator == oxc_syntax::operator::BinaryOperator::Addition =>
{
self.is_trusted_concat_prefix(&binary.left, depth + 1, visited_symbols)
}
Expression::Identifier(identifier) => {
let Some(symbol_id) = self.reference_symbol(identifier) else {
return self.is_router_co_navigated(identifier);
};
if let Some(verdict) = self.cross_file_verdict(symbol_id, false) {
return verdict;
}
if !visited_symbols.insert(symbol_id) {
return false;
}
if let Some(initializer) = self.const_initializer(symbol_id) {
if self.symbol_has_write_before_current_open(symbol_id) {
return false;
}
if matches!(
initializer.get_inner_expression(),
Expression::NewExpression(_)
) && self
.coercion_reference_for(expression)
.is_some_and(|reference| {
self.global_property_was_mutated_before("URL", "prototype", reference)
|| self.url_receiver_was_mutated_before(expression, reference)
})
{
return false;
}
return self.is_trusted_or_nullish(
Some(initializer),
depth + 1,
visited_symbols,
);
}
self.is_let_assigned_only_trusted(symbol_id, depth + 1, visited_symbols)
|| self.is_trusted_destructured_iteration(symbol_id, depth + 1, visited_symbols)
|| self.is_trusted_local_component_prop(symbol_id, depth + 1, visited_symbols)
|| self.is_trusted_use_state_binding(symbol_id, depth + 1, visited_symbols)
|| self.is_trusted_local_parameter(identifier, depth + 1, visited_symbols)
|| self.is_router_co_navigated(identifier)
}
Expression::TemplateLiteral(template) => {
let first_text = template
.quasis
.first()
.map_or("", |quasi| quasi.value.raw.as_str())
.trim_start_matches(is_ecmascript_whitespace);
if !first_text.is_empty() || template.expressions.is_empty() {
return false;
}
let Some(first_expression) = template.expressions.first() else {
return false;
};
let trusted_first_expression =
self.terminal_const_alias_initializer(first_expression);
let suffix = template
.quasis
.get(1)
.map_or("", |quasi| quasi.value.raw.as_str());
let previous_reference = self.local_serialization_reference.replace(
self.expression_node(expression)
.and_then(|node| self.execution_reference_for_node(node))
.map(AstNode::id),
);
let verdict =
Self::is_safe_interpolated_suffix(suffix, template.expressions.len() > 1)
&& (!suffix.starts_with('/')
|| self.is_proven_safe_slash_base(
trusted_first_expression,
depth + 1,
&mut visited_symbols.clone(),
))
&& self.is_trusted_interpolated_base(
trusted_first_expression,
depth + 1,
visited_symbols,
);
self.local_serialization_reference.set(previous_reference);
verdict
}
Expression::CallExpression(call) => self.is_trusted_call(call, depth, visited_symbols),
Expression::NewExpression(new_expression) => {
if !is_proven_global_namespace_reference(&new_expression.callee, "URL", self.ctx)
|| self
.node_for_span(new_expression.span)
.is_none_or(|reference| {
self.global_namespace_was_mutated_before("URL", reference)
})
{
return false;
}
let first = new_expression
.arguments
.first()
.and_then(Argument::as_expression);
first.is_some_and(|first| {
self.is_trusted_destination(first, depth + 1, &mut visited_symbols.clone())
}) && match new_expression.arguments.get(1) {
None => true,
Some(argument) => argument.as_expression().is_some_and(|base| {
self.is_trusted_destination(base, depth + 1, visited_symbols)
}),
}
}
expression if expression.as_member_expression().is_some() => {
self.is_trusted_member(expression, depth, visited_symbols)
}
_ => self.is_same_origin_location_read(expression),
}
}
fn is_trusted_static_destination(&self, expression: &Expression<'a>) -> bool {
match expression {
Expression::StringLiteral(_) => true,
Expression::TemplateLiteral(template) => {
if template.expressions.is_empty() {
return true;
}
let first_text = template
.quasis
.first()
.map_or("", |quasi| quasi.value.raw.as_str())
.trim_start_matches(is_ecmascript_whitespace);
if first_text.is_empty() {
return false;
}
let lower = first_text.to_ascii_lowercase();
["mailto:", "tel:", "sms:", "file:"]
.iter()
.any(|scheme| lower.starts_with(scheme))
|| Self::complete_origin_prefix(first_text)
|| Self::starts_unambiguous_same_origin_path(first_text)
}
_ => false,
}
}
fn is_trusted_foreign_static_text(text: &str) -> bool {
let text = text.trim_start_matches(is_ecmascript_whitespace);
if text.is_empty() {
return false;
}
let lower = text.to_ascii_lowercase();
["mailto:", "tel:", "sms:", "file:"]
.iter()
.any(|scheme| lower.starts_with(scheme))
|| Self::starts_same_origin_path(text)
}
fn starts_same_origin_path(text: &str) -> bool {
if let Some(rest) = text.strip_prefix('/') {
return !rest.starts_with('/') && !rest.starts_with('\\');
}
if ["./", "../", "?", "#"]
.iter()
.any(|prefix| text.starts_with(prefix))
{
return true;
}
let mut saw_segment = false;
for character in text.chars() {
if character == '/' || character == '?' || character == '#' {
return saw_segment;
}
if character == ':'
|| !(character.is_ascii_alphanumeric() || "_.~%-".contains(character))
{
return false;
}
saw_segment = true;
}
false
}
fn starts_unambiguous_same_origin_path(text: &str) -> bool {
text != "/" && Self::starts_same_origin_path(text)
}
fn complete_origin_prefix(text: &str) -> bool {
let lower = text.to_ascii_lowercase();
let Some(rest) = lower
.strip_prefix("http://")
.or_else(|| lower.strip_prefix("https://"))
else {
return false;
};
rest.find(['/', '?', '#']).is_some_and(|index| index > 0)
}
fn is_trusted_concat_prefix(
&self,
expression: &Expression<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
if depth > MAX_RESOLUTION_DEPTH {
return false;
}
let mut leftmost = expression.get_inner_expression();
while let Expression::BinaryExpression(binary) = leftmost {
if binary.operator != oxc_syntax::operator::BinaryOperator::Addition {
break;
}
leftmost = binary.left.get_inner_expression();
}
match leftmost {
Expression::StringLiteral(literal) => {
Self::static_text_pins_concat(literal.value.as_str())
}
Expression::TemplateLiteral(template) => {
self.is_trusted_static_destination(leftmost)
|| template.expressions.is_empty()
&& template.quasis.first().is_some_and(|quasi| {
Self::static_text_pins_concat(quasi.value.raw.as_str())
})
}
Expression::Identifier(identifier) => {
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if !visited_symbols.insert(symbol_id)
|| self.symbol_has_write_before_current_open(symbol_id)
{
return false;
}
self.const_initializer(symbol_id)
.is_some_and(|initializer| {
self.is_trusted_concat_prefix(initializer, depth + 1, visited_symbols)
})
}
_ => false,
}
}
fn static_text_pins_concat(text: &str) -> bool {
let text = text.trim_start_matches(is_ecmascript_whitespace);
if text.is_empty() {
return false;
}
let lower = text.to_ascii_lowercase();
["mailto:", "tel:", "sms:", "file:"]
.iter()
.any(|scheme| lower.starts_with(scheme))
|| Self::complete_origin_prefix(text)
|| Self::starts_unambiguous_same_origin_path(text)
}
fn is_trusted_call(
&self,
call: &CallExpression<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
if let Some(member) = call.callee.get_inner_expression().as_member_expression() {
if member.static_property_name() == Some("createObjectURL")
&& is_proven_global_namespace_reference(member.object(), "URL", self.ctx)
&& self.node_for_span(call.span).is_some_and(|reference| {
!self.global_namespace_was_mutated_before("URL", reference)
&& !self.global_property_was_mutated_before(
"URL",
"createObjectURL",
reference,
)
})
{
return true;
}
if member.static_property_name() == Some("getURL")
&& member
.object()
.get_inner_expression()
.as_member_expression()
.is_some_and(|runtime| {
runtime.static_property_name() == Some("runtime")
&& is_proven_global_namespace_reference(
runtime.object(),
"chrome",
self.ctx,
)
&& self.node_for_span(call.span).is_some_and(|reference| {
!self.global_namespace_was_mutated_before("chrome", reference)
&& !self.global_property_was_mutated_before(
"chrome", "runtime", reference,
)
})
})
{
return true;
}
if matches!(
member.static_property_name(),
Some("toString" | "toJSON" | "trim" | "trimEnd" | "trimStart")
) {
if matches!(member.static_property_name(), Some("toString" | "toJSON"))
&& self.is_global_url_instance_expression(member.object())
&& self
.node_for_span(call.span)
.and_then(|node| self.execution_reference_for_node(node))
.is_some_and(|reference| {
self.global_property_was_mutated_before("URL", "prototype", reference)
|| self.url_receiver_was_mutated_before(member.object(), reference)
})
{
return false;
}
return self.is_trusted_destination(member.object(), depth + 1, visited_symbols);
}
}
if let Expression::Identifier(identifier) = call.callee.get_inner_expression()
&& let Some(function_node) = self.resolve_local_function(identifier)
{
let Some(returns) = self.function_return_expressions(function_node) else {
return false;
};
let previous_reference = self
.local_serialization_reference
.replace(self.node_for_span(call.span).map(AstNode::id));
let verdict = !returns.is_empty()
&& returns.into_iter().all(|returned| {
self.is_trusted_local_return(
returned,
function_node,
call,
depth + 1,
&mut visited_symbols.clone(),
)
});
self.local_serialization_reference.set(previous_reference);
return verdict;
}
if let Expression::Identifier(identifier) = call.callee.get_inner_expression()
&& Self::is_cross_file_url_helper_name(identifier.name.as_str())
&& let Some(symbol_id) = self.reference_symbol(identifier)
&& let Some(verdict) = self.cross_file_verdict(symbol_id, true)
{
return verdict;
}
false
}
fn is_trusted_local_return(
&self,
returned: &Expression<'a>,
function_node: &AstNode<'a>,
call: &CallExpression<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
if let Expression::Identifier(identifier) = returned.get_inner_expression()
&& let Some(argument) = self.parameter_argument(identifier, function_node, call)
{
return self.is_trusted_or_nullish(Some(argument), depth + 1, visited_symbols);
}
if let Expression::BinaryExpression(binary) = returned.get_inner_expression()
&& binary.operator == oxc_syntax::operator::BinaryOperator::Addition
{
let mut operands = Vec::new();
Self::flatten_concat_operands(returned, &mut operands);
if let Some(Expression::Identifier(first_identifier)) = operands
.first()
.map(|operand| operand.get_inner_expression())
&& let Some(argument) =
self.parameter_argument(first_identifier, function_node, call)
{
let mut suffix = String::new();
let mut following_expression = false;
for operand in operands.iter().skip(1) {
let Some(text) = Self::static_string_text(operand) else {
following_expression = true;
break;
};
suffix.push_str(text);
}
if Self::is_safe_interpolated_suffix(&suffix, following_expression)
&& (!suffix.starts_with('/')
|| self.is_proven_safe_slash_base(
argument,
depth + 1,
&mut visited_symbols.clone(),
))
{
return self.is_trusted_destination(argument, depth + 1, visited_symbols);
}
}
}
if let Expression::TemplateLiteral(template) = returned.get_inner_expression()
&& template
.quasis
.first()
.is_none_or(|quasi| quasi.value.raw.is_empty())
&& let Some(Expression::Identifier(first_identifier)) = template
.expressions
.first()
.map(Expression::get_inner_expression)
&& let Some(argument) = self.parameter_argument(first_identifier, function_node, call)
{
let suffix = template
.quasis
.get(1)
.map_or("", |quasi| quasi.value.raw.as_str());
if Self::is_safe_interpolated_suffix(&suffix, template.expressions.len() > 1)
&& (!suffix.starts_with('/')
|| self.is_proven_safe_slash_base(
argument,
depth + 1,
&mut visited_symbols.clone(),
))
{
return self.is_trusted_destination(argument, depth + 1, visited_symbols);
}
}
self.is_trusted_or_nullish(Some(returned), depth, visited_symbols)
}
fn flatten_concat_operands<'expression>(
expression: &'expression Expression<'a>,
operands: &mut Vec<&'expression Expression<'a>>,
) {
if let Expression::BinaryExpression(binary) = expression.get_inner_expression()
&& binary.operator == oxc_syntax::operator::BinaryOperator::Addition
{
Self::flatten_concat_operands(&binary.left, operands);
Self::flatten_concat_operands(&binary.right, operands);
} else {
operands.push(expression.get_inner_expression());
}
}
fn static_string_text<'value>(expression: &'value Expression<'_>) -> Option<&'value str> {
match expression.get_inner_expression() {
Expression::StringLiteral(literal) => Some(literal.value.as_str()),
Expression::TemplateLiteral(template) if template.expressions.is_empty() => template
.quasis
.first()
.map(|quasi| quasi.value.raw.as_str()),
_ => None,
}
}
fn parameter_argument<'call>(
&self,
identifier: &oxc_ast::ast::IdentifierReference<'a>,
function_node: &AstNode<'a>,
call: &'call CallExpression<'a>,
) -> Option<&'call Expression<'a>> {
let symbol_id = self.reference_symbol(identifier)?;
let parameter_index = match function_node.kind() {
AstKind::Function(function) => function.params.items.iter().position(|parameter| {
parameter
.pattern
.get_binding_identifiers()
.iter()
.any(|binding| binding.symbol_id() == symbol_id)
}),
AstKind::ArrowFunctionExpression(arrow) => {
arrow.params.items.iter().position(|parameter| {
parameter
.pattern
.get_binding_identifiers()
.iter()
.any(|binding| binding.symbol_id() == symbol_id)
})
}
_ => None,
}?;
call.arguments
.get(parameter_index)
.and_then(Argument::as_expression)
}
fn is_trusted_member(
&self,
expression: &Expression<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let Some(member) = expression.as_member_expression() else {
return false;
};
if self.is_same_origin_location_read(expression) {
return true;
}
let Some(property_name) = member.static_property_name() else {
return self.is_trusted_array_index(member, depth + 1, visited_symbols);
};
if property_name == "href"
&& !member.is_computed()
&& matches!(
member.object().get_inner_expression(),
Expression::Identifier(_)
)
&& self.is_trusted_url_instance(member.object(), expression, depth + 1, visited_symbols)
{
return true;
}
if property_name == "href"
&& !member.is_computed()
&& self.is_trusted_anchor_parameter(member.object(), depth + 1, visited_symbols)
{
return true;
}
let Expression::Identifier(receiver) = member.object().get_inner_expression() else {
return false;
};
let Some(symbol_id) = self.reference_symbol(receiver) else {
return false;
};
if self.symbol_has_write_before_current_open(symbol_id)
|| self.current_open.is_some_and(|open_id| {
has_possible_static_property_write_before(
receiver,
property_name,
self.ctx.nodes().get_node(open_id),
&self.property_write_analysis,
self.ctx,
)
})
{
return false;
}
let Some(Expression::ObjectExpression(object)) = self
.const_initializer(symbol_id)
.map(Expression::get_inner_expression)
else {
return self.is_trusted_iteration_member(
receiver,
property_name,
depth,
visited_symbols,
);
};
self.object_supplies_trusted_property(object, property_name, depth, visited_symbols)
}
fn is_trusted_array_index(
&self,
member: &oxc_ast::ast::MemberExpression<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let oxc_ast::ast::MemberExpression::ComputedMemberExpression(computed) = member else {
return false;
};
let Expression::NumericLiteral(index_literal) = computed.expression.get_inner_expression()
else {
return false;
};
if index_literal.value < 0.0 || index_literal.value.fract() != 0.0 {
return false;
}
let element_index = index_literal.value as usize;
let receiver = &computed.object;
let Expression::Identifier(identifier) = receiver.get_inner_expression() else {
return false;
};
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if self.symbol_has_write_before_current_open(symbol_id) {
return false;
}
if self.current_open.is_some_and(|open_id| {
self.array_symbol_was_mutated_before(
symbol_id,
self.ctx.nodes().get_node(open_id),
None,
)
}) {
return false;
}
let Some(initializer) = self.const_initializer(symbol_id) else {
return false;
};
match initializer.get_inner_expression() {
Expression::ArrayExpression(array) => {
self.array_element_is_trusted(array, element_index, depth, visited_symbols)
}
Expression::CallExpression(call) => {
let Expression::Identifier(callee) = call.callee.get_inner_expression() else {
return false;
};
let Some(function_node) = self.resolve_local_function(callee) else {
return false;
};
let Some(returned) = self.function_return_expressions(function_node) else {
return false;
};
!returned.is_empty()
&& returned.iter().all(|returned_expression| {
let Expression::ArrayExpression(array) =
returned_expression.get_inner_expression()
else {
return false;
};
self.array_element_is_trusted(
array,
element_index,
depth,
&mut visited_symbols.clone(),
)
})
}
_ => false,
}
}
fn array_element_is_trusted(
&self,
array: &oxc_ast::ast::ArrayExpression<'a>,
element_index: usize,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let Some(element) = array.elements.get(element_index) else {
return false;
};
match element {
ArrayExpressionElement::Elision(_) | ArrayExpressionElement::SpreadElement(_) => false,
element => {
self.is_trusted_destination(element.to_expression(), depth + 1, visited_symbols)
}
}
}
fn is_trusted_iteration_member(
&self,
identifier: &oxc_ast::ast::IdentifierReference<'a>,
property_name: &str,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if self.symbol_has_write_before_current_open(symbol_id) {
return false;
}
let declaration = self.ctx.symbol_declaration(symbol_id);
let Some(function_node) = self.enclosing_function(declaration) else {
return false;
};
if self
.function_parameter_for_symbol(function_node, symbol_id)
.is_none()
{
return false;
}
let parent = self.ctx.nodes().parent_node(function_node.id());
let AstKind::CallExpression(call) = parent.kind() else {
return false;
};
if !matches!(
Self::terminal_callee_name(&call.callee),
Some("map" | "forEach")
) {
return false;
}
let Some(member) = call.callee.get_inner_expression().as_member_expression() else {
return false;
};
let array_expression = match member.object().get_inner_expression() {
Expression::ArrayExpression(array) => Some(array),
Expression::Identifier(array_identifier) => {
let Some(array_symbol) = self.reference_symbol(array_identifier) else {
return false;
};
if self.current_open.is_some_and(|open_id| {
self.array_symbol_was_mutated_before(
array_symbol,
self.ctx.nodes().get_node(open_id),
Some(property_name),
)
}) {
return false;
}
self.const_initializer(array_symbol)
.and_then(|initializer| match initializer.get_inner_expression() {
Expression::ArrayExpression(array) => Some(array),
_ => None,
})
}
_ => None,
};
let Some(array) = array_expression else {
return false;
};
!array.elements.is_empty()
&& array.elements.iter().all(|element| {
let Expression::ObjectExpression(object) =
element.to_expression().get_inner_expression()
else {
return false;
};
self.object_supplies_trusted_property(
object,
property_name,
depth,
&mut visited_symbols.clone(),
)
})
}
fn is_trusted_destructured_iteration(
&self,
symbol_id: SymbolId,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let declaration = self.ctx.symbol_declaration(symbol_id);
let Some(function_node) = self.enclosing_function(declaration) else {
return false;
};
let Some((_, parameter_pattern)) =
self.function_parameter_for_symbol(function_node, symbol_id)
else {
return false;
};
let Some(property_name) = binding_property_name_for_symbol(parameter_pattern, symbol_id)
else {
return false;
};
let parent = self.ctx.nodes().parent_node(function_node.id());
let AstKind::CallExpression(call) = parent.kind() else {
return false;
};
if !matches!(
Self::terminal_callee_name(&call.callee),
Some("map" | "forEach")
) {
return false;
}
let Some(member) = call.callee.get_inner_expression().as_member_expression() else {
return false;
};
let array = match member.object().get_inner_expression() {
Expression::ArrayExpression(array) => Some(array),
Expression::Identifier(identifier) => {
let Some(array_symbol) = self.reference_symbol(identifier) else {
return false;
};
if self.symbol_has_write_before_current_open(array_symbol)
|| self.current_open.is_some_and(|open_id| {
self.array_symbol_was_mutated_before(
array_symbol,
self.ctx.nodes().get_node(open_id),
Some(property_name.as_str()),
)
})
|| self.current_open.is_some_and(|open_id| {
has_possible_static_property_write_before(
identifier,
property_name.as_str(),
self.ctx.nodes().get_node(open_id),
&self.property_write_analysis,
self.ctx,
)
})
{
return false;
}
match self
.const_initializer(array_symbol)
.map(Expression::get_inner_expression)
{
Some(Expression::ArrayExpression(array)) => Some(array),
_ => None,
}
}
_ => None,
};
let Some(array) = array else {
return false;
};
self.array_every_object_property_trusted(
array,
property_name.as_str(),
depth,
visited_symbols,
)
}
fn array_every_object_property_trusted(
&self,
array: &oxc_ast::ast::ArrayExpression<'a>,
property_name: &str,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
!array.elements.is_empty()
&& array.elements.iter().all(|element| match element {
ArrayExpressionElement::Elision(_) | ArrayExpressionElement::SpreadElement(_) => {
false
}
element => {
let Expression::ObjectExpression(object) =
element.to_expression().get_inner_expression()
else {
return false;
};
self.object_supplies_trusted_property(
object,
property_name,
depth,
&mut visited_symbols.clone(),
)
}
})
}
fn object_supplies_trusted_property(
&self,
object: &oxc_ast::ast::ObjectExpression<'a>,
property_name: &str,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let mut trusted_value = None;
for property in &object.properties {
match property {
ObjectPropertyKind::ObjectProperty(property)
if property.key.static_name().as_deref() == Some(property_name) =>
{
trusted_value = Some(self.is_trusted_destination(
&property.value,
depth + 1,
&mut visited_symbols.clone(),
));
}
ObjectPropertyKind::SpreadProperty(_) => trusted_value = Some(false),
ObjectPropertyKind::ObjectProperty(property)
if property.computed && property.key.static_name().is_none() =>
{
trusted_value = Some(false);
}
_ => {}
}
}
trusted_value == Some(true)
}
fn is_trusted_local_component_prop(
&self,
symbol_id: SymbolId,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let declaration = self.ctx.symbol_declaration(symbol_id);
let Some(component_function) = self.enclosing_function(declaration) else {
return false;
};
let Some((_, parameter_pattern)) =
self.function_parameter_for_symbol(component_function, symbol_id)
else {
return false;
};
let Some(property_name) = binding_property_name_for_symbol(parameter_pattern, symbol_id)
else {
return false;
};
let Some(component_symbol) = self.local_function_symbol(component_function) else {
return false;
};
let component_name = self.ctx.scoping().symbol_name(component_symbol);
if !component_name
.as_bytes()
.first()
.is_some_and(u8::is_ascii_uppercase)
|| self.is_exported_node(component_function)
{
return false;
}
for reference in self.ctx.scoping().get_resolved_references(component_symbol) {
let reference_node = self.ctx.nodes().get_node(reference.node_id());
let is_jsx_name = self
.ctx
.nodes()
.ancestors(reference_node.id())
.any(|ancestor| match ancestor.kind() {
AstKind::JSXOpeningElement(opening) => {
opening.name.span() == reference_node.span()
}
AstKind::JSXClosingElement(closing) => {
closing.name.span() == reference_node.span()
}
_ => false,
});
if !is_jsx_name {
return false;
}
}
let mut usage_count = 0usize;
for node in self.ctx.nodes().iter() {
let AstKind::JSXOpeningElement(opening) = node.kind() else {
continue;
};
let JSXElementName::Identifier(element_name) = &opening.name else {
continue;
};
if element_name.name != component_name {
continue;
}
usage_count += 1;
if opening.attributes.iter().any(|attribute| {
matches!(
attribute,
oxc_ast::ast::JSXAttributeItem::SpreadAttribute(_)
)
}) {
return false;
}
let Some(attribute) =
get_authoritative_jsx_attribute(opening, property_name.as_str(), true)
else {
continue;
};
let Some(value) = &attribute.value else {
return false;
};
match value {
JSXAttributeValue::StringLiteral(_) => {}
JSXAttributeValue::ExpressionContainer(container) => {
let Some(expression) = container.expression.as_expression() else {
return false;
};
if !self.is_trusted_or_nullish(
Some(expression),
depth + 1,
&mut visited_symbols.clone(),
) {
return false;
}
}
_ => return false,
}
}
usage_count > 0
}
fn is_trusted_use_state_binding(
&self,
state_symbol_id: SymbolId,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let declaration = self.ctx.symbol_declaration(state_symbol_id);
let Some(declarator_node) = self
.ctx
.nodes()
.ancestors(declaration.id())
.find(|ancestor| matches!(ancestor.kind(), AstKind::VariableDeclarator(_)))
else {
return false;
};
let AstKind::VariableDeclarator(declarator) = declarator_node.kind() else {
return false;
};
if !matches!(
self.ctx.nodes().parent_node(declarator_node.id()).kind(),
AstKind::VariableDeclaration(declaration) if declaration.kind.is_const()
) {
return false;
}
let BindingPattern::ArrayPattern(array_pattern) = &declarator.id else {
return false;
};
let Some(initializer) = declarator.init.as_ref() else {
return false;
};
let Expression::CallExpression(state_call) = initializer.get_inner_expression() else {
return false;
};
if Self::terminal_callee_name(&state_call.callee) != Some("useState") {
return false;
}
let state_matches = array_pattern
.elements
.first()
.and_then(Option::as_ref)
.is_some_and(|pattern| binding_pattern_has_symbol(pattern, state_symbol_id));
if !state_matches {
return false;
}
if let Some(initial_state) = state_call
.arguments
.first()
.and_then(Argument::as_expression)
&& !self.is_trusted_or_nullish(
Some(initial_state),
depth + 1,
&mut visited_symbols.clone(),
)
{
return false;
}
let Some(setter_pattern) = array_pattern.elements.get(1).and_then(Option::as_ref) else {
return false;
};
let Some(setter) = setter_pattern.get_binding_identifier() else {
return false;
};
let setter_symbol = setter.symbol_id();
for reference in self.ctx.scoping().get_resolved_references(setter_symbol) {
let reference_node = self.ctx.nodes().get_node(reference.node_id());
let root = self.transparent_node_root(reference_node);
let parent = self.ctx.nodes().parent_node(root.id());
let AstKind::CallExpression(call) = parent.kind() else {
return false;
};
if call.callee.span() != root.span() {
return false;
}
let Some(value) = call.arguments.first().and_then(Argument::as_expression) else {
return false;
};
if !self.is_trusted_or_nullish(Some(value), depth + 1, &mut visited_symbols.clone()) {
return false;
}
}
true
}
fn is_react_api_call(&self, call: &CallExpression<'a>, api_name: &str) -> bool {
match call.callee.get_inner_expression() {
Expression::Identifier(identifier) => self.reference_symbol(identifier).is_some_and(
|symbol_id| {
self.ctx.module_record().import_entries.iter().any(|entry| {
entry.module_request.name() == "react"
&& self
.ctx
.scoping()
.get_root_binding(entry.local_name.name().into())
== Some(symbol_id)
&& matches!(&entry.import_name, ImportImportName::Name(name) if name.name() == api_name)
})
},
),
expression => expression.as_member_expression().is_some_and(|member| {
member.static_property_name() == Some(api_name)
&& self.identifier_is_react_namespace(
member.object(),
&mut FxHashSet::default(),
)
}),
}
}
fn identifier_is_react_namespace(
&self,
expression: &Expression<'a>,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let Expression::Identifier(identifier) = expression.get_inner_expression() else {
return false;
};
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if !visited_symbols.insert(symbol_id) {
return false;
}
if self.ctx.module_record().import_entries.iter().any(|entry| {
entry.module_request.name() == "react"
&& self
.ctx
.scoping()
.get_root_binding(entry.local_name.name().into())
== Some(symbol_id)
&& matches!(
&entry.import_name,
ImportImportName::Default(_) | ImportImportName::NamespaceObject
)
}) {
return true;
}
self.const_initializer(symbol_id)
.is_some_and(|initializer| {
self.identifier_is_react_namespace(initializer, visited_symbols)
})
}
fn function_parameter_for_symbol(
&self,
function_node: &AstNode<'a>,
symbol_id: SymbolId,
) -> Option<(usize, &BindingPattern<'a>)> {
let parameters = match function_node.kind() {
AstKind::Function(function) => &function.params.items,
AstKind::ArrowFunctionExpression(arrow) => &arrow.params.items,
_ => return None,
};
parameters
.iter()
.enumerate()
.find_map(|(index, parameter)| {
binding_pattern_has_symbol(&parameter.pattern, symbol_id)
.then_some((index, &parameter.pattern))
})
}
fn is_trusted_url_instance(
&self,
receiver: &Expression<'a>,
serialization_expression: &Expression<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let serialization_reference = self
.expression_node(serialization_expression)
.and_then(|node| self.execution_reference_for_node(node));
if serialization_reference.is_some_and(|reference| {
self.global_property_was_mutated_before("URL", "prototype", reference)
|| self.url_receiver_was_mutated_before(receiver, reference)
}) {
return false;
}
match receiver.get_inner_expression() {
Expression::NewExpression(_) => {
self.is_trusted_destination(receiver, depth + 1, visited_symbols)
}
Expression::Identifier(identifier) => {
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if self.symbol_has_write_before_current_open(symbol_id) {
return false;
}
self.const_initializer(symbol_id)
.is_some_and(|initializer| {
matches!(
initializer.get_inner_expression(),
Expression::NewExpression(_)
) && self.is_trusted_destination(initializer, depth + 1, visited_symbols)
})
}
_ => false,
}
}
fn is_global_url_instance_expression(&self, expression: &Expression<'a>) -> bool {
match expression.get_inner_expression() {
Expression::NewExpression(new_expression) => {
is_proven_global_namespace_reference(&new_expression.callee, "URL", self.ctx)
}
Expression::Identifier(identifier) => self
.reference_symbol(identifier)
.and_then(|symbol_id| self.const_initializer(symbol_id))
.is_some_and(|initializer| self.is_global_url_instance_expression(initializer)),
_ => false,
}
}
fn is_trusted_anchor_parameter(
&self,
receiver: &Expression<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let Expression::Identifier(identifier) = receiver.get_inner_expression() else {
return false;
};
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
let declaration = self.ctx.symbol_declaration(symbol_id);
let Some(function_node) = self.enclosing_function(declaration) else {
return false;
};
let Some((parameter_index, _)) =
self.function_parameter_for_symbol(function_node, symbol_id)
else {
return false;
};
if self.current_open.is_some_and(|open_id| {
has_possible_static_property_write_before(
identifier,
"href",
self.ctx.nodes().get_node(open_id),
&self.property_write_analysis,
self.ctx,
)
}) {
return false;
}
let Some(calls) = self.direct_named_local_function_calls(function_node) else {
return false;
};
!calls.is_empty()
&& calls.iter().all(|call_id| {
let AstKind::CallExpression(call) = self.ctx.nodes().get_node(*call_id).kind()
else {
return false;
};
let Some(argument) = call
.arguments
.get(parameter_index)
.and_then(Argument::as_expression)
else {
return false;
};
let Some(member) = argument.get_inner_expression().as_member_expression() else {
return false;
};
if member.static_property_name() != Some("currentTarget") {
return false;
}
let Expression::Identifier(event_identifier) =
member.object().get_inner_expression()
else {
return false;
};
let Some(event_symbol) = self.reference_symbol(event_identifier) else {
return false;
};
let event_declaration = self.ctx.symbol_declaration(event_symbol);
let Some(handler_function) = self.enclosing_function(event_declaration) else {
return false;
};
self.function_parameter_for_symbol(handler_function, event_symbol)
.is_some()
&& self.handler_only_serves_trusted_anchors(
handler_function,
depth + 1,
&mut visited_symbols.clone(),
)
})
}
fn handler_only_serves_trusted_anchors(
&self,
handler_function: &AstNode<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let parent = self.ctx.nodes().parent_node(handler_function.id());
if matches!(parent.kind(), AstKind::JSXExpressionContainer(_)) {
return self
.jsx_event_handler_opening_element(parent)
.is_some_and(|opening| {
self.jsx_anchor_has_trusted_href(opening, depth, visited_symbols)
});
}
let Some(handler_symbol) = self.local_function_symbol(handler_function) else {
return false;
};
let mut usage_count = 0usize;
for reference in self.ctx.scoping().get_resolved_references(handler_symbol) {
let reference_node = self.ctx.nodes().get_node(reference.node_id());
let root = self.transparent_node_root(reference_node);
let parent = self.ctx.nodes().parent_node(root.id());
let Some(opening) = self.jsx_event_handler_opening_element(parent) else {
return false;
};
usage_count += 1;
if !self.jsx_anchor_has_trusted_href(opening, depth, &mut visited_symbols.clone()) {
return false;
}
}
usage_count > 0
}
fn jsx_event_handler_opening_element(
&self,
expression_container: &AstNode<'a>,
) -> Option<&oxc_ast::ast::JSXOpeningElement<'a>> {
let AstKind::JSXExpressionContainer(_) = expression_container.kind() else {
return None;
};
let attribute_node = self.ctx.nodes().parent_node(expression_container.id());
let AstKind::JSXAttribute(attribute) = attribute_node.kind() else {
return None;
};
if !Self::jsx_attribute_name(attribute).is_some_and(Self::is_event_handler_name) {
return None;
}
let opening_node = self.ctx.nodes().parent_node(attribute_node.id());
let AstKind::JSXOpeningElement(opening) = opening_node.kind() else {
return None;
};
Some(opening)
}
fn jsx_anchor_has_trusted_href(
&self,
opening: &oxc_ast::ast::JSXOpeningElement<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
if !matches!(&opening.name, JSXElementName::Identifier(identifier) if identifier.name == "a")
{
return false;
}
let Some(attribute) = get_authoritative_jsx_attribute(opening, "href", true) else {
return false;
};
let Some(value) = &attribute.value else {
return false;
};
match value {
JSXAttributeValue::StringLiteral(_) => true,
JSXAttributeValue::ExpressionContainer(container) => container
.expression
.as_expression()
.is_some_and(|expression| {
self.is_trusted_destination(expression, depth + 1, &mut visited_symbols.clone())
}),
_ => false,
}
}
fn is_same_origin_location_read(&self, expression: &Expression<'a>) -> bool {
let Some(member) = expression.as_member_expression() else {
return false;
};
if member.is_computed() {
return false;
}
let Some(property_name) = member.static_property_name() else {
return false;
};
if property_name == "origin"
&& is_proven_global_namespace_reference(member.object(), "window", self.ctx)
{
return true;
}
if !matches!(property_name, "origin" | "href") {
return false;
}
self.is_location_receiver(member.object(), &mut FxHashSet::default())
}
fn is_location_receiver(
&self,
expression: &Expression<'a>,
visited_functions: &mut FxHashSet<NodeId>,
) -> bool {
if is_proven_global_namespace_reference(expression, "location", self.ctx) {
return true;
}
let Expression::CallExpression(call) = expression.get_inner_expression() else {
return false;
};
let Expression::Identifier(identifier) = call.callee.get_inner_expression() else {
return false;
};
let Some(function_node) = self.resolve_local_function(identifier) else {
return false;
};
if !visited_functions.insert(function_node.id()) {
return false;
}
let Some(returns) = self.function_return_expressions(function_node) else {
return false;
};
!returns.is_empty()
&& returns
.iter()
.all(|returned| self.is_location_receiver(returned, &mut visited_functions.clone()))
}
fn global_namespace_was_mutated_before(
&self,
namespace_name: &str,
reference: &AstNode<'a>,
) -> bool {
let has_mutation = self.ctx.nodes().iter().any(|candidate| {
if !can_node_execute_before(
candidate,
reference,
&self.property_write_analysis,
self.ctx,
) {
return false;
}
let direct_mutation = match candidate.kind() {
AstKind::AssignmentExpression(assignment) => match &assignment.left {
oxc_ast::ast::AssignmentTarget::AssignmentTargetIdentifier(identifier) => {
identifier.name == namespace_name
&& self
.ctx
.scoping()
.get_reference(identifier.reference_id())
.symbol_id()
.is_none()
}
target => target.as_member_expression().is_some_and(|member| {
self.member_chain_starts_at_global(member, namespace_name)
}),
},
AstKind::UpdateExpression(update) => update
.argument
.as_member_expression()
.is_some_and(|member| {
self.member_chain_starts_at_global(member, namespace_name)
}),
AstKind::UnaryExpression(unary) if unary.operator == UnaryOperator::Delete => {
unary.argument.as_member_expression().is_some_and(|member| {
self.member_chain_starts_at_global(member, namespace_name)
})
}
_ => false,
};
if direct_mutation {
return true;
}
let AstKind::CallExpression(call) = candidate.kind() else {
return false;
};
if self.global_object_mutation_method_name(call).is_none() {
return false;
}
let Some(target) = call.arguments.first().and_then(Argument::as_expression) else {
return false;
};
is_proven_global_namespace_reference(target, namespace_name, self.ctx)
|| target
.get_inner_expression()
.as_member_expression()
.is_some_and(|member| {
self.member_chain_starts_at_global(member, namespace_name)
})
});
has_mutation
}
fn element_has_router_contract(
&self,
destination_symbol: SymbolId,
current_open: &AstNode<'a>,
) -> bool {
let Some(opening_node) = self
.ctx
.nodes()
.ancestors(current_open.id())
.find(|ancestor| matches!(ancestor.kind(), AstKind::JSXOpeningElement(_)))
else {
return false;
};
let mut matching_opens = Vec::new();
let mut matching_router_calls = Vec::new();
for candidate in self.ctx.nodes().iter() {
if !opening_node.span().contains_inclusive(candidate.span()) {
continue;
}
let AstKind::CallExpression(call) = candidate.kind() else {
continue;
};
let argument_symbol = call
.arguments
.first()
.and_then(Argument::as_expression)
.and_then(|argument| match argument.get_inner_expression() {
Expression::Identifier(identifier) => self.reference_symbol(identifier),
_ => None,
});
if argument_symbol != Some(destination_symbol) {
continue;
}
if self.is_window_open_call(candidate, call) {
matching_opens.push(candidate);
continue;
}
let Some(member) = call.callee.get_inner_expression().as_member_expression() else {
continue;
};
if matches!(member.static_property_name(), Some("push" | "replace"))
&& self.is_next_router_receiver(member.object())
&& self.enclosing_function(candidate).is_some_and(|function| {
is_node_reachable_within_function(candidate, function, self.ctx)
})
{
matching_router_calls.push(candidate);
}
}
matching_opens.iter().any(|open| {
matching_router_calls
.iter()
.any(|router| self.calls_execute_in_opposite_branches(open, router))
})
}
fn global_property_was_mutated_before(
&self,
namespace_name: &str,
property_name: &str,
reference: &AstNode<'a>,
) -> bool {
self.ctx.nodes().iter().any(|candidate| {
if !can_node_execute_before(
candidate,
reference,
&self.property_write_analysis,
self.ctx,
) {
return false;
}
let member = match candidate.kind() {
AstKind::AssignmentExpression(assignment) => assignment.left.as_member_expression(),
AstKind::UpdateExpression(update) => update.argument.as_member_expression(),
AstKind::UnaryExpression(unary) if unary.operator == UnaryOperator::Delete => {
unary.argument.as_member_expression()
}
_ => None,
};
if member.is_some_and(|member| {
self.member_chain_starts_at_global(member, namespace_name)
&& self.member_chain_contains_property(member, property_name)
}) {
return true;
}
let AstKind::CallExpression(call) = candidate.kind() else {
return false;
};
let Some(method_name) = self.global_object_mutation_method_name(call) else {
return false;
};
let Some(target) = call.arguments.first().and_then(Argument::as_expression) else {
return false;
};
if is_proven_global_namespace_reference(target, namespace_name, self.ctx)
&& self.mutation_call_may_write_property(call, method_name, Some(property_name))
{
return true;
}
let target_matches = target
.get_inner_expression()
.as_member_expression()
.is_some_and(|target_member| {
self.member_chain_starts_at_global(target_member, namespace_name)
&& self.member_chain_contains_property(target_member, property_name)
});
target_matches && self.mutation_call_may_write_property(call, method_name, None)
})
}
fn global_object_mutation_method_name<'call>(
&self,
call: &'call CallExpression<'a>,
) -> Option<&'call str> {
const OBJECT_METHODS: [&str; 4] = [
"assign",
"defineProperties",
"defineProperty",
"setPrototypeOf",
];
const REFLECT_METHODS: [&str; 3] = ["defineProperty", "set", "setPrototypeOf"];
let callee = call.callee.get_inner_expression();
let method_name = Self::terminal_callee_name(callee)?;
if !OBJECT_METHODS.contains(&method_name) && !REFLECT_METHODS.contains(&method_name) {
return None;
}
if let Some(member) = callee.as_member_expression() {
let is_object_method = OBJECT_METHODS.contains(&method_name)
&& is_proven_global_namespace_reference(member.object(), "Object", self.ctx);
let is_reflect_method = REFLECT_METHODS.contains(&method_name)
&& is_proven_global_namespace_reference(member.object(), "Reflect", self.ctx);
return (is_object_method || is_reflect_method).then_some(method_name);
}
let Expression::Identifier(identifier) = callee else {
return None;
};
let is_object_alias = OBJECT_METHODS.contains(&method_name)
&& self.identifier_resolves_to_global_method(
identifier,
"Object",
method_name,
&mut FxHashSet::default(),
);
let is_reflect_alias = REFLECT_METHODS.contains(&method_name)
&& self.identifier_resolves_to_global_method(
identifier,
"Reflect",
method_name,
&mut FxHashSet::default(),
);
(is_object_alias || is_reflect_alias).then_some(method_name)
}
fn identifier_resolves_to_global_method(
&self,
identifier: &oxc_ast::ast::IdentifierReference<'a>,
namespace_name: &str,
method_name: &str,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if !visited_symbols.insert(symbol_id) {
return false;
}
let declaration = self.ctx.symbol_declaration(symbol_id);
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return false;
};
if !matches!(
self.ctx.nodes().parent_node(declaration.id()).kind(),
AstKind::VariableDeclaration(variable) if variable.kind.is_const()
) {
return false;
}
let Some(initializer) = &declarator.init else {
return false;
};
if binding_property_name_for_symbol(&declarator.id, symbol_id).as_deref()
== Some(method_name)
&& is_proven_global_namespace_reference(initializer, namespace_name, self.ctx)
{
return true;
}
match initializer.get_inner_expression() {
Expression::Identifier(alias) => self.identifier_resolves_to_global_method(
alias,
namespace_name,
method_name,
visited_symbols,
),
expression => expression.as_member_expression().is_some_and(|member| {
member.static_property_name() == Some(method_name)
&& is_proven_global_namespace_reference(
member.object(),
namespace_name,
self.ctx,
)
}),
}
}
fn mutation_call_may_write_property(
&self,
call: &CallExpression<'a>,
method_name: &str,
property_name: Option<&str>,
) -> bool {
match method_name {
"defineProperty" | "set" => {
let Some(property_name) = property_name else {
return true;
};
call.arguments
.get(1)
.and_then(Argument::as_expression)
.and_then(Self::static_mutation_property_name)
.is_none_or(|written_name| written_name == property_name)
}
"assign" => {
let Some(property_name) = property_name else {
return true;
};
call.arguments.iter().skip(1).any(|argument| {
let Some(Expression::ObjectExpression(object)) = argument
.as_expression()
.map(Expression::get_inner_expression)
else {
return true;
};
object.properties.iter().any(|property| match property {
ObjectPropertyKind::ObjectProperty(property) => property
.key
.static_name()
.is_none_or(|name| name.as_ref() == property_name),
ObjectPropertyKind::SpreadProperty(_) => true,
})
})
}
"defineProperties" => {
let Some(property_name) = property_name else {
return true;
};
let Some(argument) = call.arguments.get(1).and_then(Argument::as_expression) else {
return true;
};
let Expression::ObjectExpression(object) = argument.get_inner_expression() else {
return true;
};
object.properties.iter().any(|property| match property {
ObjectPropertyKind::ObjectProperty(property) => property
.key
.static_name()
.is_none_or(|name| name.as_ref() == property_name),
ObjectPropertyKind::SpreadProperty(_) => true,
})
}
"setPrototypeOf" => true,
_ => false,
}
}
fn member_chain_starts_at_global(
&self,
member: &oxc_ast::ast::MemberExpression<'a>,
namespace_name: &str,
) -> bool {
if member.static_property_name() == Some(namespace_name)
&& ["global", "globalThis", "self", "window"]
.iter()
.any(|receiver_name| {
is_proven_global_namespace_reference(member.object(), receiver_name, self.ctx)
})
{
return true;
}
let object = member.object().get_inner_expression();
self.expression_chain_starts_at_global(object, namespace_name, &mut FxHashSet::default())
}
fn expression_chain_starts_at_global(
&self,
expression: &Expression<'a>,
namespace_name: &str,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
if is_proven_global_namespace_reference(expression, namespace_name, self.ctx) {
return true;
}
if let Some(member) = expression.get_inner_expression().as_member_expression() {
return self.expression_chain_starts_at_global(
member.object(),
namespace_name,
visited_symbols,
);
}
let Expression::Identifier(identifier) = expression.get_inner_expression() else {
return false;
};
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if !visited_symbols.insert(symbol_id) {
return false;
}
let declaration = self.ctx.symbol_declaration(symbol_id);
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return false;
};
let Some(initializer) = &declarator.init else {
return false;
};
self.expression_chain_starts_at_global(initializer, namespace_name, visited_symbols)
}
fn member_chain_contains_property(
&self,
member: &oxc_ast::ast::MemberExpression<'a>,
property_name: &str,
) -> bool {
member.static_property_name() == Some(property_name)
|| self.expression_chain_contains_property(
member.object(),
property_name,
&mut FxHashSet::default(),
)
|| member
.object()
.get_inner_expression()
.as_member_expression()
.is_some_and(|inner| self.member_chain_contains_property(inner, property_name))
}
fn expression_chain_contains_property(
&self,
expression: &Expression<'a>,
property_name: &str,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
if let Some(member) = expression.get_inner_expression().as_member_expression() {
return member.static_property_name() == Some(property_name)
|| self.expression_chain_contains_property(
member.object(),
property_name,
visited_symbols,
);
}
let Expression::Identifier(identifier) = expression.get_inner_expression() else {
return false;
};
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if !visited_symbols.insert(symbol_id) {
return false;
}
let declaration = self.ctx.symbol_declaration(symbol_id);
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return false;
};
if binding_property_name_for_symbol(&declarator.id, symbol_id).as_deref()
== Some(property_name)
{
return true;
}
declarator.init.as_ref().is_some_and(|initializer| {
self.expression_chain_contains_property(initializer, property_name, visited_symbols)
})
}
fn url_receiver_was_mutated_before(
&self,
receiver: &Expression<'a>,
reference: &AstNode<'a>,
) -> bool {
let Expression::Identifier(identifier) = receiver.get_inner_expression() else {
return false;
};
[
"href", "host", "hostname", "password", "port", "protocol", "toJSON", "toString",
"username",
]
.iter()
.any(|property_name| {
has_possible_static_property_write_before(
identifier,
property_name,
reference,
&self.property_write_analysis,
self.ctx,
)
}) || self.object_mutation_call_targets_receiver(identifier, reference)
}
fn object_mutation_call_targets_receiver(
&self,
receiver: &oxc_ast::ast::IdentifierReference<'a>,
reference: &AstNode<'a>,
) -> bool {
let Some(receiver_symbol) = resolve_const_identifier_root_symbol(receiver, self.ctx) else {
return false;
};
self.ctx.nodes().iter().any(|candidate| {
let AstKind::CallExpression(call) = candidate.kind() else {
return false;
};
let Some(method_name) = self.global_object_mutation_method_name(call) else {
return false;
};
let Some(Expression::Identifier(target)) = call
.arguments
.first()
.and_then(Argument::as_expression)
.map(Expression::get_inner_expression)
else {
return false;
};
resolve_const_identifier_root_symbol(target, self.ctx) == Some(receiver_symbol)
&& can_node_execute_before(
candidate,
reference,
&self.property_write_analysis,
self.ctx,
)
&& [
"href", "host", "hostname", "password", "port", "protocol", "toJSON",
"toString", "username",
]
.iter()
.any(|property_name| {
self.mutation_call_may_write_property(call, method_name, Some(property_name))
})
})
}
fn coercion_reference_for(&self, expression: &Expression<'a>) -> Option<&AstNode<'a>> {
if let Some(reference_id) = self.local_serialization_reference.get() {
return Some(self.ctx.nodes().get_node(reference_id));
}
let open_node = self
.current_open
.map(|open_id| self.ctx.nodes().get_node(open_id));
let expression_node = open_node.or_else(|| self.expression_node(expression))?;
self.execution_reference_for_node(expression_node)
}
fn execution_reference_for_node<'node>(
&'node self,
expression_node: &'node AstNode<'a>,
) -> Option<&'node AstNode<'a>> {
if let Some(reference_id) = self.local_serialization_reference.get() {
return Some(self.ctx.nodes().get_node(reference_id));
}
let Some(function_node) = self.enclosing_function(expression_node) else {
return Some(expression_node);
};
self.direct_local_function_calls(function_node)
.unwrap_or_default()
.into_iter()
.max_by_key(|call_id| self.ctx.nodes().get_node(*call_id).span().start)
.map(|call_id| self.ctx.nodes().get_node(call_id))
.or(Some(expression_node))
}
fn array_symbol_was_mutated_before(
&self,
symbol_id: SymbolId,
reference: &AstNode<'a>,
nested_property_name: Option<&str>,
) -> bool {
const MUTATING_METHODS: [&str; 9] = [
"copyWithin",
"fill",
"pop",
"push",
"reverse",
"shift",
"sort",
"splice",
"unshift",
];
self.const_alias_symbols(symbol_id)
.into_iter()
.any(|alias| {
self.ctx.scoping().get_resolved_references(alias).any(|symbol_reference| {
let identifier_node = self.ctx.nodes().get_node(symbol_reference.node_id());
let identifier_root = self.transparent_node_root(identifier_node);
let immediate_parent = self.ctx.nodes().parent_node(identifier_root.id());
let Some(immediate_object_span) =
Self::ast_member_object_span(immediate_parent.kind())
else {
return false;
};
if immediate_object_span != identifier_root.span() {
return false;
}
let member_root = self.transparent_node_root(immediate_parent);
let member_parent = self.ctx.nodes().parent_node(member_root.id());
if let AstKind::CallExpression(call) = member_parent.kind()
&& call.callee.span() == member_root.span()
&& MUTATING_METHODS.iter().any(|method_name| {
Self::ast_member_has_static_property(
immediate_parent.kind(),
method_name,
)
})
{
return can_node_execute_before(
member_parent,
reference,
&self.property_write_analysis,
self.ctx,
);
}
let mut chain_node = immediate_parent;
let mut chain_contains_property = nested_property_name.is_none();
loop {
if let Some(property_name) = nested_property_name
&& Self::ast_member_has_static_property(chain_node.kind(), property_name)
{
chain_contains_property = true;
}
let chain_root = self.transparent_node_root(chain_node);
let parent = self.ctx.nodes().parent_node(chain_root.id());
if Self::ast_member_object_span(parent.kind()) == Some(chain_root.span())
{
chain_node = parent;
continue;
}
let is_write = matches!(parent.kind(), AstKind::AssignmentExpression(assignment)
if assignment.left.span() == chain_root.span())
|| matches!(parent.kind(), AstKind::UpdateExpression(update)
if update.argument.span() == chain_root.span())
|| matches!(parent.kind(), AstKind::UnaryExpression(unary)
if unary.operator == UnaryOperator::Delete
&& unary.argument.span() == chain_root.span());
return chain_contains_property
&& is_write
&& can_node_execute_before(
parent,
reference,
&self.property_write_analysis,
self.ctx,
);
}
})
})
}
fn ast_member_object_span(kind: AstKind<'a>) -> Option<Span> {
match kind {
AstKind::StaticMemberExpression(member) => Some(member.object.span()),
AstKind::ComputedMemberExpression(member) => Some(member.object.span()),
AstKind::PrivateFieldExpression(member) => Some(member.object.span()),
_ => None,
}
}
fn ast_member_has_static_property(kind: AstKind<'a>, property_name: &str) -> bool {
match kind {
AstKind::StaticMemberExpression(member) => member.property.name == property_name,
AstKind::ComputedMemberExpression(member) => {
member.static_property_name().as_deref() == Some(property_name)
}
AstKind::PrivateFieldExpression(_) => false,
_ => false,
}
}
fn resolve_local_function(
&self,
identifier: &oxc_ast::ast::IdentifierReference<'a>,
) -> Option<&AstNode<'a>> {
let symbol_id = self.reference_symbol(identifier)?;
if self.symbol_has_write_before_current_open(symbol_id) {
return None;
}
let declaration = self.ctx.symbol_declaration(symbol_id);
match declaration.kind() {
AstKind::Function(_) if !self.is_exported_node(declaration) => Some(declaration),
AstKind::VariableDeclarator(declarator) if matches!(self.ctx.nodes().parent_node(declaration.id()).kind(), AstKind::VariableDeclaration(variable) if variable.kind.is_const()) =>
{
let initializer = declarator.init.as_ref()?.get_inner_expression();
match initializer {
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) => {
self.expression_node(initializer)
}
Expression::CallExpression(call)
if Self::terminal_callee_name(&call.callee) == Some("useCallback") =>
{
call.arguments
.first()
.and_then(Argument::as_expression)
.and_then(|expression| self.expression_node(expression))
}
_ => None,
}
}
_ => None,
}
}
fn function_return_expressions(
&self,
function_node: &AstNode<'a>,
) -> Option<Vec<&Expression<'a>>> {
if let AstKind::ArrowFunctionExpression(arrow) = function_node.kind()
&& !matches!(&arrow.body, ArrowFunctionBody::FunctionBody(_))
{
return Some(vec![arrow.body.to_expression()]);
}
let mut returned_expressions = Vec::new();
for node in self.ctx.nodes().iter() {
let AstKind::ReturnStatement(statement) = node.kind() else {
continue;
};
if !self
.enclosing_function(node)
.is_some_and(|enclosing| enclosing.id() == function_node.id())
{
continue;
}
let Some(argument) = statement.argument.as_ref() else {
return None;
};
returned_expressions.push(argument);
}
Some(returned_expressions)
}
fn is_trusted_local_parameter(
&self,
identifier: &oxc_ast::ast::IdentifierReference<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if self.symbol_has_write_before_current_open(symbol_id) {
return false;
}
let declaration = self.ctx.symbol_declaration(symbol_id);
let Some(function_node) = self.enclosing_function(declaration) else {
return false;
};
let parameter_index = match function_node.kind() {
AstKind::Function(function) => function.params.items.iter().position(|parameter| {
parameter
.pattern
.get_binding_identifiers()
.iter()
.any(|binding| binding.symbol_id() == symbol_id)
}),
AstKind::ArrowFunctionExpression(arrow) => {
arrow.params.items.iter().position(|parameter| {
parameter
.pattern
.get_binding_identifiers()
.iter()
.any(|binding| binding.symbol_id() == symbol_id)
})
}
_ => None,
};
let Some(parameter_index) = parameter_index else {
return false;
};
let Some(calls) = self.direct_named_local_function_calls(function_node) else {
return false;
};
!calls.is_empty()
&& calls.iter().all(|call_id| {
let AstKind::CallExpression(call) = self.ctx.nodes().get_node(*call_id).kind()
else {
return false;
};
call.arguments
.get(parameter_index)
.and_then(Argument::as_expression)
.is_some_and(|argument| {
self.is_trusted_or_nullish(
Some(argument),
depth + 1,
&mut visited_symbols.clone(),
)
})
})
}
fn is_let_assigned_only_trusted(
&self,
symbol_id: SymbolId,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let declaration = self.ctx.symbol_declaration(symbol_id);
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return false;
};
if matches!(self.ctx.nodes().parent_node(declaration.id()).kind(), AstKind::VariableDeclaration(variable) if variable.kind.is_const())
{
return false;
}
if declarator.init.as_ref().is_some_and(|initializer| {
!self.is_trusted_destination(initializer, depth + 1, &mut visited_symbols.clone())
}) {
return false;
}
let mut saw_assignment = false;
for reference in self.ctx.scoping().get_resolved_references(symbol_id) {
if reference.is_read() {
continue;
}
let reference_node = self.ctx.nodes().get_node(reference.node_id());
let root = self.transparent_node_root(reference_node);
let parent = self.ctx.nodes().parent_node(root.id());
let AstKind::AssignmentExpression(assignment) = parent.kind() else {
return false;
};
if assignment.operator != AssignmentOperator::Assign
|| assignment.left.span() != root.span()
{
return false;
}
saw_assignment = true;
if !self.is_trusted_destination(
&assignment.right,
depth + 1,
&mut visited_symbols.clone(),
) {
return false;
}
}
saw_assignment
}
fn is_router_co_navigated(&self, identifier: &oxc_ast::ast::IdentifierReference<'a>) -> bool {
let Some(open_id) = self.current_open else {
return false;
};
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
let open_node = self.ctx.nodes().get_node(open_id);
let Some(function) = self.enclosing_function(open_node) else {
return false;
};
let has_same_function_contract = self.ctx.nodes().iter().any(|candidate| {
let AstKind::CallExpression(call) = candidate.kind() else {
return false;
};
if self
.enclosing_function(candidate)
.is_none_or(|owner| owner.id() != function.id())
|| !is_node_reachable_within_function(candidate, function, self.ctx)
|| !self.calls_execute_in_opposite_branches(open_node, candidate)
{
return false;
}
let Some(member) = call.callee.get_inner_expression().as_member_expression() else {
return false;
};
if !matches!(member.static_property_name(), Some("push" | "replace"))
|| !self.is_next_router_receiver(member.object())
{
return false;
}
call.arguments
.first()
.and_then(Argument::as_expression)
.and_then(|argument| match argument.get_inner_expression() {
Expression::Identifier(argument_identifier) => {
self.reference_symbol(argument_identifier)
}
_ => None,
})
== Some(symbol_id)
});
has_same_function_contract || self.element_has_router_contract(symbol_id, open_node)
}
fn is_next_router_receiver(&self, receiver: &Expression<'a>) -> bool {
let Expression::Identifier(identifier) = receiver.get_inner_expression() else {
return false;
};
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
self.ctx.module_record().import_entries.iter().any(|entry| {
self.ctx
.scoping()
.get_root_binding(entry.local_name.name().into())
== Some(symbol_id)
&& entry.module_request.name() == "next/router"
&& matches!(entry.import_name, ImportImportName::Default(_))
}) || self
.const_initializer(symbol_id)
.is_some_and(|initializer| {
let Expression::CallExpression(call) = initializer.get_inner_expression() else {
return false;
};
let Expression::Identifier(callee) = call.callee.get_inner_expression() else {
return false;
};
let Some(callee_symbol) = self.reference_symbol(callee) else {
return false;
};
self.ctx.module_record().import_entries.iter().any(|entry| {
self.ctx
.scoping()
.get_root_binding(entry.local_name.name().into())
== Some(callee_symbol)
&& matches!(entry.module_request.name(), "next/router" | "next/navigation")
&& matches!(&entry.import_name, ImportImportName::Name(name) if name.name() == "useRouter")
})
})
}
fn calls_execute_in_opposite_branches(&self, left: &AstNode<'a>, right: &AstNode<'a>) -> bool {
self.ctx
.nodes()
.ancestors(left.id())
.any(|ancestor| match ancestor.kind() {
AstKind::IfStatement(statement) => {
let left_is_consequent =
statement.consequent.span().contains_inclusive(left.span());
let left_is_alternate = statement
.alternate
.as_ref()
.is_some_and(|alternate| alternate.span().contains_inclusive(left.span()));
(left_is_consequent
&& statement.alternate.as_ref().is_some_and(|alternate| {
alternate.span().contains_inclusive(right.span())
}))
|| (left_is_alternate
&& statement.consequent.span().contains_inclusive(right.span()))
}
AstKind::ConditionalExpression(conditional) => {
(conditional
.consequent
.span()
.contains_inclusive(left.span())
&& conditional
.alternate
.span()
.contains_inclusive(right.span()))
|| (conditional.alternate.span().contains_inclusive(left.span())
&& conditional
.consequent
.span()
.contains_inclusive(right.span()))
}
_ => false,
})
}
fn is_proven_safe_slash_base(
&self,
expression: &Expression<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
if depth > MAX_RESOLUTION_DEPTH {
return false;
}
match expression.get_inner_expression() {
Expression::StringLiteral(literal) => {
let without_line_whitespace: String = literal
.value
.chars()
.filter(|character| !matches!(character, '\t' | '\n' | '\r'))
.collect();
let normalized = without_line_whitespace
.trim_matches(|character| character <= ' ')
.replace('\\', "/");
let normalized_lowercase = normalized.to_ascii_lowercase();
let is_incomplete_scheme =
["http:", "https:", "ftp:", "ws:", "wss:"]
.iter()
.any(|prefix| {
normalized_lowercase
.strip_prefix(prefix)
.is_some_and(|suffix| {
suffix.chars().all(|character| character == '/')
})
});
!normalized.is_empty()
&& normalized.chars().any(|character| character != '/')
&& !is_incomplete_scheme
}
Expression::BooleanLiteral(_)
| Expression::NumericLiteral(_)
| Expression::BigIntLiteral(_)
| Expression::NullLiteral(_)
| Expression::UnaryExpression(_) => {
self.static_primitive_truthiness(expression).is_some()
}
Expression::ConditionalExpression(conditional) => {
if let Some(test_truthiness) = self.static_primitive_truthiness(&conditional.test) {
return self.is_proven_safe_slash_base(
if test_truthiness {
&conditional.consequent
} else {
&conditional.alternate
},
depth + 1,
visited_symbols,
);
}
self.is_proven_safe_slash_base(
&conditional.consequent,
depth + 1,
&mut visited_symbols.clone(),
) && self.is_proven_safe_slash_base(
&conditional.alternate,
depth + 1,
&mut visited_symbols.clone(),
)
}
Expression::LogicalExpression(logical) => {
let left_truthiness = self.static_primitive_truthiness(&logical.left);
match logical.operator {
LogicalOperator::And => {
left_truthiness == Some(false)
|| self.is_proven_safe_slash_base(
&logical.right,
depth + 1,
visited_symbols,
)
}
LogicalOperator::Coalesce if self.is_nullish(Some(&logical.left)) => {
self.is_proven_safe_slash_base(&logical.right, depth + 1, visited_symbols)
}
LogicalOperator::Coalesce if left_truthiness.is_some() => {
self.is_proven_safe_slash_base(&logical.left, depth + 1, visited_symbols)
}
LogicalOperator::Or if left_truthiness.is_some() => self
.is_proven_safe_slash_base(
if left_truthiness == Some(true) {
&logical.left
} else {
&logical.right
},
depth + 1,
visited_symbols,
),
_ => {
self.is_proven_safe_slash_base(
&logical.left,
depth + 1,
&mut visited_symbols.clone(),
) && self.is_proven_safe_slash_base(
&logical.right,
depth + 1,
&mut visited_symbols.clone(),
)
}
}
}
Expression::Identifier(identifier) => {
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if !visited_symbols.insert(symbol_id) {
return false;
}
self.const_initializer(symbol_id)
.is_some_and(|initializer| {
self.is_proven_safe_slash_base(initializer, depth + 1, visited_symbols)
})
}
Expression::CallExpression(call) => {
let Expression::Identifier(callee) = call.callee.get_inner_expression() else {
return false;
};
let Some(function_node) = self.resolve_local_function(callee) else {
return false;
};
let Some(returns) = self.function_return_expressions(function_node) else {
return false;
};
!returns.is_empty()
&& returns.iter().all(|returned| {
self.is_proven_safe_slash_base(
returned,
depth + 1,
&mut visited_symbols.clone(),
)
})
}
expression => self.is_same_origin_location_read(expression),
}
}
fn static_primitive_truthiness(&self, expression: &Expression<'a>) -> Option<bool> {
if self.is_nullish(Some(expression)) {
return Some(false);
}
match expression.get_inner_expression() {
Expression::BooleanLiteral(literal) => Some(literal.value),
Expression::StringLiteral(literal) => Some(!literal.value.is_empty()),
Expression::NumericLiteral(literal) => {
Some(literal.value != 0.0 && !literal.value.is_nan())
}
Expression::BigIntLiteral(literal) => Some(!literal.is_zero()),
_ => None,
}
}
fn terminal_const_alias_initializer<'expression>(
&'expression self,
expression: &'expression Expression<'a>,
) -> &'expression Expression<'a> {
let mut current = expression;
let mut visited_symbols = FxHashSet::default();
loop {
let Expression::Identifier(identifier) = current.get_inner_expression() else {
return current;
};
let Some(symbol_id) = self.reference_symbol(identifier) else {
return current;
};
if !visited_symbols.insert(symbol_id) {
return current;
}
let Some(initializer) = self.const_initializer(symbol_id) else {
return current;
};
current = initializer;
}
}
fn is_trusted_interpolated_base(
&self,
expression: &Expression<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let expression = expression.get_inner_expression();
if !matches!(expression, Expression::StringLiteral(_))
&& self.static_primitive_truthiness(expression).is_some()
{
return true;
}
match expression {
Expression::ConditionalExpression(conditional) => {
if let Some(test_truthiness) = self.static_primitive_truthiness(&conditional.test) {
return self.is_trusted_interpolated_base(
if test_truthiness {
&conditional.consequent
} else {
&conditional.alternate
},
depth + 1,
visited_symbols,
);
}
self.is_trusted_interpolated_base(
&conditional.consequent,
depth + 1,
&mut visited_symbols.clone(),
) && self.is_trusted_interpolated_base(
&conditional.alternate,
depth + 1,
&mut visited_symbols.clone(),
)
}
Expression::LogicalExpression(logical) => {
let left_truthiness = self.static_primitive_truthiness(&logical.left);
if logical.operator == LogicalOperator::And && left_truthiness == Some(false) {
return true;
}
if logical.operator == LogicalOperator::Coalesce
&& self.is_nullish(Some(&logical.left))
{
return self.is_trusted_interpolated_base(
&logical.right,
depth + 1,
visited_symbols,
);
}
if let Some(left_truthiness) = left_truthiness {
let branch = if logical.operator == LogicalOperator::Or && !left_truthiness {
&logical.right
} else {
&logical.left
};
return self.is_trusted_interpolated_base(branch, depth + 1, visited_symbols);
}
self.is_trusted_interpolated_base(
&logical.left,
depth + 1,
&mut visited_symbols.clone(),
) && self.is_trusted_interpolated_base(
&logical.right,
depth + 1,
&mut visited_symbols.clone(),
)
}
_ => self.is_trusted_destination(expression, depth + 1, visited_symbols),
}
}
fn is_statically_truthy_trusted(
&self,
expression: &Expression<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
match expression.get_inner_expression() {
Expression::StringLiteral(literal) => !literal.value.is_empty(),
Expression::TemplateLiteral(template) => {
template
.quasis
.iter()
.any(|quasi| !quasi.value.raw.is_empty())
&& self.is_trusted_static_destination(expression.get_inner_expression())
}
Expression::Identifier(identifier) => {
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if !visited_symbols.insert(symbol_id) {
return false;
}
self.const_initializer(symbol_id)
.is_some_and(|initializer| {
self.is_statically_truthy_trusted(initializer, depth + 1, visited_symbols)
})
}
_ => false,
}
}
fn is_nullish(&self, expression: Option<&Expression<'a>>) -> bool {
let Some(expression) = expression else {
return true;
};
match expression.get_inner_expression() {
Expression::NullLiteral(_) => true,
Expression::UnaryExpression(unary) => unary.operator == UnaryOperator::Void,
Expression::Identifier(identifier) if identifier.name == "undefined" => {
self.reference_symbol(identifier).is_none()
}
_ => false,
}
}
fn is_raw_nullish(&self, expression: &Expression<'a>) -> bool {
match expression {
Expression::NullLiteral(_) => true,
Expression::UnaryExpression(unary) => unary.operator == UnaryOperator::Void,
Expression::Identifier(identifier) if identifier.name == "undefined" => {
self.reference_symbol(identifier).is_none()
}
_ => false,
}
}
fn resolve_static_string(
&self,
expression: &Expression<'a>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> Option<String> {
if depth > MAX_RESOLUTION_DEPTH {
return None;
}
match expression {
Expression::StringLiteral(literal) => Some(literal.value.to_string()),
Expression::TemplateLiteral(template) => {
let mut text = String::new();
for (index, quasi) in template.quasis.iter().enumerate() {
text.push_str(quasi.value.raw.as_str());
if let Some(interpolation) = template.expressions.get(index) {
text.push_str(
self.resolve_static_string(
interpolation,
depth + 1,
&mut visited_symbols.clone(),
)
.as_deref()
.unwrap_or("\0"),
);
}
}
Some(text)
}
Expression::Identifier(identifier) => {
let symbol_id = self.reference_symbol(identifier)?;
if !visited_symbols.insert(symbol_id) {
return None;
}
self.const_initializer(symbol_id).and_then(|initializer| {
self.resolve_static_string(initializer, depth + 1, visited_symbols)
})
}
_ => None,
}
}
fn features_may_protect_opener(features: &str) -> bool {
features
.split(|character: char| is_ecmascript_whitespace(character) || character == ',')
.any(|entry| {
let lowercase_entry = entry.to_ascii_lowercase();
let mut parts = lowercase_entry.split('=');
let name = parts.next().unwrap_or_default();
let value = parts.next();
let opaque_name = !name.is_empty()
&& name
.chars()
.all(|character| character == OPAQUE_FEATURE_TEXT);
let value_may_enable = value.is_none_or(|value| {
matches!(value, "1" | "true" | "yes")
|| (!value.is_empty()
&& value
.chars()
.all(|character| character == OPAQUE_FEATURE_TEXT))
});
(matches!(name, "noopener" | "noreferrer") || opaque_name) && value_may_enable
})
}
fn const_initializer(&self, symbol_id: SymbolId) -> Option<&Expression<'a>> {
let declaration = self.ctx.symbol_declaration(symbol_id);
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return None;
};
if !matches!(self.ctx.nodes().parent_node(declaration.id()).kind(), AstKind::VariableDeclaration(variable) if variable.kind.is_const())
|| declarator
.id
.get_binding_identifier()
.is_none_or(|identifier| identifier.symbol_id() != symbol_id)
{
return None;
}
declarator.init.as_ref()
}
fn reference_symbol(
&self,
identifier: &oxc_ast::ast::IdentifierReference<'a>,
) -> Option<SymbolId> {
self.ctx
.scoping()
.get_reference(identifier.reference_id())
.symbol_id()
}
fn symbol_has_write_before_current_open(&self, symbol_id: SymbolId) -> bool {
let Some(open_id) = self.current_open else {
return true;
};
self.symbol_has_write_before(symbol_id, self.ctx.nodes().get_node(open_id))
}
fn symbol_has_write_before(&self, symbol_id: SymbolId, reference: &AstNode<'a>) -> bool {
self.ctx
.scoping()
.get_resolved_references(symbol_id)
.any(|symbol_reference| {
!symbol_reference.is_read()
&& can_node_execute_before(
self.ctx.nodes().get_node(symbol_reference.node_id()),
reference,
&self.property_write_analysis,
self.ctx,
)
})
}
fn cross_file_verdict(&self, symbol_id: SymbolId, expect_function: bool) -> Option<bool> {
if !self.ctx.file_path().is_absolute() {
return None;
}
let import_entry = self
.ctx
.module_record()
.import_entries
.iter()
.find(|entry| {
!entry.is_type
&& self
.ctx
.scoping()
.get_root_binding(entry.local_name.name().into())
== Some(symbol_id)
})?;
let exported_name = match &import_entry.import_name {
ImportImportName::Name(name) => name.name(),
ImportImportName::Default(_) => "default",
ImportImportName::NamespaceObject => return None,
};
let module_source = import_entry.module_request.name();
let memo_key = (
module_source.to_string(),
exported_name.to_string(),
expect_function,
);
if let Some(verdict) = self.cross_file_memo.borrow().get(&memo_key) {
return *verdict;
}
let budget_key = (module_source.to_string(), exported_name.to_string());
if !self.cross_file_seen.borrow().contains(&budget_key) {
let remaining = self.cross_file_remaining.get();
if remaining == 0 {
return None;
}
self.cross_file_seen.borrow_mut().insert(budget_key);
self.cross_file_remaining.set(remaining - 1);
}
let verdict = resolve_window_open_cross_file_export(
self.ctx.file_path(),
module_source,
exported_name,
expect_function,
0,
&mut FxHashSet::default(),
);
self.cross_file_memo.borrow_mut().insert(memo_key, verdict);
verdict
}
fn is_cross_file_url_helper_name(name: &str) -> bool {
["get", "create", "build"].iter().any(|prefix| {
name.strip_prefix(prefix).is_some_and(|remainder| {
["Url", "URL"].iter().any(|suffix| {
remainder.strip_suffix(suffix).is_some_and(|middle| {
middle
.chars()
.all(|character| character.is_ascii_alphanumeric())
})
})
})
})
}
fn string_literal_value<'value>(expression: &'value Expression<'_>) -> Option<&'value str> {
match expression.get_inner_expression() {
Expression::StringLiteral(literal) => Some(literal.value.as_str()),
_ => None,
}
}
fn static_mutation_property_name<'value>(
expression: &'value Expression<'_>,
) -> Option<&'value str> {
match expression.get_inner_expression() {
Expression::StringLiteral(literal) => Some(literal.value.as_str()),
Expression::TemplateLiteral(template) if template.expressions.is_empty() => template
.quasis
.first()
.and_then(|quasi| quasi.value.cooked.as_ref())
.map(|value| value.as_str()),
_ => None,
}
}
fn terminal_callee_name<'value>(expression: &'value Expression<'_>) -> Option<&'value str> {
match expression.get_inner_expression() {
Expression::Identifier(identifier) => Some(identifier.name.as_str()),
expression => expression
.as_member_expression()
.and_then(|member| member.static_property_name()),
}
}
fn jsx_attribute_name<'value>(
attribute: &'value oxc_ast::ast::JSXAttribute<'_>,
) -> Option<&'value str> {
match &attribute.name {
oxc_ast::ast::JSXAttributeName::Identifier(identifier) => {
Some(identifier.name.as_str())
}
_ => None,
}
}
fn is_event_handler_name(name: &str) -> bool {
name.strip_prefix("on")
.and_then(|suffix| suffix.as_bytes().first())
.is_some_and(u8::is_ascii_uppercase)
}
fn is_safe_interpolated_suffix(suffix: &str, has_following_expression: bool) -> bool {
if suffix.is_empty() {
return !has_following_expression;
}
if suffix.starts_with('?') || suffix.starts_with('#') {
return true;
}
suffix.starts_with('/') && !suffix.starts_with("//") && !suffix.starts_with("/\\")
}
}
fn is_ecmascript_whitespace(character: char) -> bool {
matches!(
character,
'\u{0009}'
| '\u{000a}'
| '\u{000b}'
| '\u{000c}'
| '\u{000d}'
| '\u{0020}'
| '\u{00a0}'
| '\u{1680}'
| '\u{2000}'
| '\u{2001}'
| '\u{2002}'
| '\u{2003}'
| '\u{2004}'
| '\u{2005}'
| '\u{2006}'
| '\u{2007}'
| '\u{2008}'
| '\u{2009}'
| '\u{200a}'
| '\u{2028}'
| '\u{2029}'
| '\u{202f}'
| '\u{205f}'
| '\u{3000}'
| '\u{feff}'
)
}
pub(super) fn resolve_window_open_module_path(
from_file: &Path,
module_source: &str,
) -> Option<PathBuf> {
if Path::new(module_source).is_absolute()
|| (!module_source.starts_with('.')
&& !window_open_tsconfig_allows_bare_import(from_file, module_source))
{
return None;
}
let resolver = Resolver::new(ResolveOptions {
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"]
.into_iter()
.map(String::from)
.collect(),
main_fields: vec!["module".into(), "main".into(), "browser".into()],
condition_names: vec![
"import".into(),
"default".into(),
"module".into(),
"browser".into(),
"require".into(),
],
extension_alias: vec![
(
".js".into(),
vec![".js".into(), ".ts".into(), ".tsx".into(), ".jsx".into()],
),
(".jsx".into(), vec![".jsx".into(), ".tsx".into()]),
(".mjs".into(), vec![".mjs".into(), ".mts".into()]),
(".cjs".into(), vec![".cjs".into(), ".cts".into()]),
],
tsconfig: Some(TsconfigDiscovery::Auto),
..ResolveOptions::default()
});
let path = resolver
.resolve_file(from_file, module_source)
.ok()?
.path()
.to_path_buf();
if path
.components()
.any(|component| component.as_os_str() == "node_modules")
|| path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| {
name.ends_with(".d.ts") || name.ends_with(".d.mts") || name.ends_with(".d.cts")
})
{
return None;
}
Some(path)
}
struct WindowOpenResolvedTsconfig {
has_explicit_base_url: bool,
path_patterns: Vec<String>,
}
pub(super) fn window_open_tsconfig_allows_bare_import(
from_file: &Path,
module_source: &str,
) -> bool {
let Some(mut directory) = from_file.parent() else {
return false;
};
for _ in 0..MAX_TSCONFIG_DIRECTORY_WALK {
for filename in ["tsconfig.json", "jsconfig.json"] {
if let Some(config) = window_open_read_resolved_tsconfig(&directory.join(filename), 0) {
return config.has_explicit_base_url
|| config.path_patterns.iter().any(|pattern| {
window_open_tsconfig_pattern_matches(module_source, pattern)
});
}
}
let Some(parent) = directory.parent() else {
break;
};
if parent == directory {
break;
}
directory = parent;
}
false
}
fn window_open_read_resolved_tsconfig(
config_path: &Path,
extends_depth: usize,
) -> Option<WindowOpenResolvedTsconfig> {
let source = std::fs::read_to_string(config_path).ok()?;
let parsed = serde_json::from_str::<serde_json::Value>(
&window_open_strip_json_comments_and_trailing_commas(&source),
)
.ok()?;
let compiler_options = parsed
.get("compilerOptions")
.and_then(serde_json::Value::as_object);
let has_explicit_base_url = compiler_options
.and_then(|options| options.get("baseUrl"))
.and_then(serde_json::Value::as_str)
.is_some();
if let Some(paths) = compiler_options.and_then(|options| options.get("paths"))
&& (paths.is_object() || paths.is_array())
{
let path_patterns = match paths {
serde_json::Value::Object(entries) => entries
.iter()
.filter(|(_, targets)| {
targets
.as_array()
.is_some_and(|targets| targets.iter().any(serde_json::Value::is_string))
})
.map(|(pattern, _)| pattern.clone())
.collect(),
serde_json::Value::Array(entries) => entries
.iter()
.enumerate()
.filter(|(_, targets)| {
targets
.as_array()
.is_some_and(|targets| targets.iter().any(serde_json::Value::is_string))
})
.map(|(index, _)| index.to_string())
.collect(),
_ => Vec::new(),
};
return Some(WindowOpenResolvedTsconfig {
has_explicit_base_url,
path_patterns,
});
}
if extends_depth < MAX_TSCONFIG_EXTENDS_DEPTH
&& let Some(extends_value) = parsed.get("extends").and_then(serde_json::Value::as_str)
{
let mut extends_path =
if extends_value.starts_with("./") || extends_value.starts_with("../") {
config_path.parent()?.join(extends_value)
} else {
config_path
.parent()?
.join("node_modules")
.join(extends_value)
};
if !extends_value.ends_with(".json") {
extends_path = PathBuf::from(format!("{}.json", extends_path.display()));
}
if let Some(inherited) =
window_open_read_resolved_tsconfig(&extends_path, extends_depth + 1)
{
return Some(inherited);
}
}
has_explicit_base_url.then_some(WindowOpenResolvedTsconfig {
has_explicit_base_url: true,
path_patterns: Vec::new(),
})
}
fn window_open_tsconfig_pattern_matches(module_source: &str, pattern: &str) -> bool {
let Some(star_index) = pattern.find('*') else {
return module_source == pattern;
};
let prefix = &pattern[..star_index];
let suffix = &pattern[star_index + 1..];
module_source.len() >= prefix.len() + suffix.len()
&& module_source.starts_with(prefix)
&& module_source.ends_with(suffix)
}
pub(super) fn window_open_strip_json_comments_and_trailing_commas(source: &str) -> String {
let characters: Vec<char> = source.chars().collect();
let mut without_comments = String::with_capacity(source.len());
let mut index = 0;
let mut in_string = false;
let mut in_line_comment = false;
let mut in_block_comment = false;
while index < characters.len() {
let character = characters[index];
let next_character = characters.get(index + 1).copied();
if in_line_comment {
if character == '\n' {
in_line_comment = false;
without_comments.push(character);
}
index += 1;
continue;
}
if in_block_comment {
if character == '*' && next_character == Some('/') {
in_block_comment = false;
index += 2;
} else {
index += 1;
}
continue;
}
if in_string {
without_comments.push(character);
if character == '\\' {
if let Some(escaped) = next_character {
without_comments.push(escaped);
index += 2;
continue;
}
} else if character == '"' {
in_string = false;
}
index += 1;
continue;
}
if character == '"' {
in_string = true;
without_comments.push(character);
index += 1;
continue;
}
if character == '/' && next_character == Some('/') {
in_line_comment = true;
index += 2;
continue;
}
if character == '/' && next_character == Some('*') {
in_block_comment = true;
index += 2;
continue;
}
without_comments.push(character);
index += 1;
}
let characters: Vec<char> = without_comments.chars().collect();
let mut without_trailing_commas = String::with_capacity(without_comments.len());
let mut index = 0;
while index < characters.len() {
let character = characters[index];
if character == ',' {
let mut next_index = index + 1;
while characters
.get(next_index)
.is_some_and(|character| is_ecmascript_whitespace(*character))
{
next_index += 1;
}
if matches!(characters.get(next_index), Some('}' | ']')) {
index += 1;
continue;
}
}
without_trailing_commas.push(character);
index += 1;
}
without_trailing_commas
}
fn resolve_window_open_cross_file_export(
from_file: &Path,
module_source: &str,
exported_name: &str,
expect_function: bool,
depth: usize,
visited_paths: &mut FxHashSet<PathBuf>,
) -> Option<bool> {
let file_path = resolve_window_open_module_path(from_file, module_source)?;
evaluate_window_open_export_in_file(
&file_path,
exported_name,
expect_function,
depth,
visited_paths,
)
}
fn evaluate_window_open_export_in_file(
file_path: &Path,
exported_name: &str,
expect_function: bool,
depth: usize,
visited_paths: &mut FxHashSet<PathBuf>,
) -> Option<bool> {
if depth >= MAX_CROSS_FILE_REEXPORT_DEPTH {
return None;
}
let canonical_path = std::fs::canonicalize(file_path).ok()?;
if !visited_paths.insert(canonical_path) {
return None;
}
let metadata = std::fs::metadata(file_path).ok()?;
if !metadata.is_file() || metadata.len() > MAX_CROSS_FILE_BYTES {
return None;
}
let source = std::fs::read_to_string(file_path).ok()?;
let source_type = SourceType::from_path(file_path).ok()?;
let allocator = Allocator::default();
let parser_return = Parser::new(&allocator, &source, source_type).parse();
if parser_return.panicked || !parser_return.diagnostics.is_empty() {
return None;
}
let program = allocator.alloc(parser_return.program);
let semantic_return = SemanticBuilder::new_linter().build(program);
if !semantic_return.diagnostics.is_empty() {
return None;
}
let semantic = semantic_return.semantic;
let module_record = ModuleRecord::new(file_path, &parser_return.module_record, &semantic);
if let Some(symbol_id) = foreign_export_symbol(exported_name, &semantic, &module_record) {
let analysis = ForeignWindowOpenAnalysis {
semantic: &semantic,
allow_url_construction: Cell::new(false),
};
return if expect_function {
analysis.function_symbol_is_trusted(symbol_id)
} else {
analysis.initializer_symbol_is_trusted(symbol_id)
};
}
if exported_name == "default" {
let analysis = ForeignWindowOpenAnalysis {
semantic: &semantic,
allow_url_construction: Cell::new(false),
};
if let Some(verdict) = semantic.nodes().iter().find_map(|node| {
let AstKind::ExportDefaultDeclaration(declaration) = node.kind() else {
return None;
};
match &declaration.declaration {
ExportDefaultDeclarationKind::FunctionDeclaration(function) => {
let returns = analysis.function_returns(function.node_id.get());
Some(
expect_function
&& returns.as_ref().is_some_and(|returns| {
!returns.is_empty()
&& returns.iter().all(|returned| {
analysis.is_trusted_destination(
returned,
true,
0,
&mut FxHashSet::default(),
)
})
}),
)
}
ExportDefaultDeclarationKind::ArrowFunctionExpression(function) => {
let returns = analysis.function_returns(function.node_id.get());
Some(
expect_function
&& returns.as_ref().is_some_and(|returns| {
!returns.is_empty()
&& returns.iter().all(|returned| {
analysis.is_trusted_destination(
returned,
true,
0,
&mut FxHashSet::default(),
)
})
}),
)
}
declaration => declaration.as_expression().map(|expression| {
!expect_function
&& analysis.is_trusted_destination(
expression,
false,
0,
&mut FxHashSet::default(),
)
}),
}
}) {
return Some(verdict);
}
}
if let Some((next_source, imported_name)) =
foreign_reexport_target(exported_name, &module_record)
{
return resolve_window_open_cross_file_export(
file_path,
next_source,
imported_name,
expect_function,
depth + 1,
&mut visited_paths.clone(),
);
}
let mut resolved_export_all = None;
for statement in &program.body {
let oxc_ast::ast::Statement::ExportAllDeclaration(declaration) = statement else {
continue;
};
if declaration.export_kind.is_type() || declaration.exported.is_some() {
continue;
}
let Some(verdict) = resolve_window_open_cross_file_export(
file_path,
declaration.source.value.as_str(),
exported_name,
expect_function,
depth + 1,
&mut visited_paths.clone(),
) else {
continue;
};
if resolved_export_all.is_some() {
return None;
}
resolved_export_all = Some(verdict);
}
resolved_export_all
}
fn foreign_export_symbol(
exported_name: &str,
semantic: &Semantic<'_>,
module_record: &ModuleRecord,
) -> Option<SymbolId> {
let local_name = module_record
.local_export_entries
.iter()
.find_map(|entry| {
let is_match = match &entry.export_name {
ExportExportName::Name(name) => name.name() == exported_name,
ExportExportName::Default(_) => exported_name == "default",
ExportExportName::Null => false,
};
is_match.then(|| entry.local_name.name()).flatten()
})?;
semantic.scoping().get_root_binding(local_name.into())
}
fn foreign_reexport_target<'record>(
exported_name: &str,
module_record: &'record ModuleRecord,
) -> Option<(&'record str, &'record str)> {
module_record
.indirect_export_entries
.iter()
.find_map(|entry| {
let entry_exported_name = match &entry.export_name {
ExportExportName::Name(name) => name.name(),
ExportExportName::Default(_) => "default",
ExportExportName::Null => return None,
};
if entry_exported_name != exported_name {
return None;
}
let source = entry.module_request.as_ref()?.name();
let imported_name = match &entry.import_name {
ExportImportName::Name(name) => name.name(),
_ => return None,
};
Some((source, imported_name))
})
}
struct ForeignWindowOpenAnalysis<'semantic, 'ast> {
semantic: &'semantic Semantic<'ast>,
allow_url_construction: Cell<bool>,
}
impl<'semantic, 'ast> ForeignWindowOpenAnalysis<'semantic, 'ast> {
fn initializer_symbol_is_trusted(&self, symbol_id: SymbolId) -> Option<bool> {
let declaration = self.semantic.symbol_declaration(symbol_id);
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return None;
};
let initializer = declarator.init.as_ref()?;
if matches!(
initializer.get_inner_expression(),
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_)
) {
return None;
}
Some(self.is_trusted_destination(initializer, false, 0, &mut FxHashSet::default()))
}
fn function_symbol_is_trusted(&self, symbol_id: SymbolId) -> Option<bool> {
let function_id = self.function_id_for_symbol(symbol_id)?;
let Some(returns) = self.function_returns(function_id) else {
return Some(false);
};
if returns.is_empty() {
return Some(false);
}
Some(returns.iter().all(|returned| {
self.is_trusted_destination(returned, true, 0, &mut FxHashSet::default())
}))
}
fn is_trusted_destination(
&self,
expression: &Expression<'ast>,
deferred: bool,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
if depth > MAX_RESOLUTION_DEPTH {
return false;
}
let expression = expression.get_inner_expression();
match expression {
Expression::StringLiteral(literal) => {
WindowOpenAnalysis::is_trusted_foreign_static_text(literal.value.as_str())
}
Expression::TemplateLiteral(template) => {
if template.expressions.is_empty() {
return template.quasis.first().is_some_and(|quasi| {
WindowOpenAnalysis::is_trusted_foreign_static_text(quasi.value.raw.as_str())
});
}
let prefix = template
.quasis
.first()
.map_or("", |quasi| quasi.value.raw.as_str())
.trim_start_matches(is_ecmascript_whitespace);
if !prefix.is_empty() {
return Self::is_trusted_foreign_interpolated_prefix(prefix);
}
let Some(base) = template.expressions.first() else {
return false;
};
let suffix = template
.quasis
.get(1)
.map_or("", |quasi| quasi.value.raw.as_str());
WindowOpenAnalysis::is_safe_interpolated_suffix(
suffix,
template.expressions.len() > 1,
) && (!suffix.starts_with('/')
|| self.is_proven_safe_slash_base(
base,
deferred,
depth + 1,
&mut visited_symbols.clone(),
))
&& (!self.is_global_url_instance_expression(base)
|| (!self.global_was_mutated_before("URL", expression.span(), deferred)
&& !self.foreign_url_receiver_was_mutated_before(
base,
expression.span(),
deferred,
)))
&& self.with_url_construction_allowed(|| {
self.is_trusted_interpolated_base(
base,
deferred,
depth + 1,
visited_symbols,
)
})
}
Expression::Identifier(identifier) => {
let Some(symbol_id) = self.reference_symbol(identifier) else {
return identifier.name == "undefined";
};
if !visited_symbols.insert(symbol_id) {
return false;
}
if let Some(initializer) = self.symbol_const_initializer(symbol_id) {
return !self.symbol_was_mutated_before(symbol_id, expression.span(), deferred)
&& self.is_trusted_destination(
initializer,
deferred,
depth + 1,
visited_symbols,
);
}
self.is_foreign_let_assigned_only_trusted(
symbol_id,
expression.span(),
deferred,
depth + 1,
visited_symbols,
)
}
Expression::ConditionalExpression(conditional) => {
self.is_trusted_or_nullish(
&conditional.consequent,
deferred,
depth + 1,
&mut visited_symbols.clone(),
) && self.is_trusted_or_nullish(
&conditional.alternate,
deferred,
depth + 1,
&mut visited_symbols.clone(),
)
}
Expression::LogicalExpression(logical) => {
if logical.operator == LogicalOperator::And {
return self.is_nullish(&logical.left)
|| self.is_trusted_or_nullish(
&logical.right,
deferred,
depth + 1,
visited_symbols,
);
}
if self.is_statically_truthy_trusted(
&logical.left,
deferred,
depth + 1,
&mut visited_symbols.clone(),
) {
return true;
}
self.is_trusted_or_nullish(
&logical.left,
deferred,
depth + 1,
&mut visited_symbols.clone(),
) && self.is_trusted_or_nullish(
&logical.right,
deferred,
depth + 1,
&mut visited_symbols.clone(),
)
}
Expression::BinaryExpression(binary)
if binary.operator == oxc_syntax::operator::BinaryOperator::Addition =>
{
let mut leftmost = binary.left.get_inner_expression();
while let Expression::BinaryExpression(next) = leftmost {
if next.operator != oxc_syntax::operator::BinaryOperator::Addition {
break;
}
leftmost = next.left.get_inner_expression();
}
self.is_trusted_concat_prefix(leftmost, deferred, depth + 1, visited_symbols)
}
Expression::NewExpression(new_expression) => {
self.allow_url_construction.get()
&& self.is_global_reference(&new_expression.callee, "URL")
&& !self.global_was_mutated_before("URL", new_expression.span, deferred)
&& new_expression
.arguments
.first()
.and_then(Argument::as_expression)
.is_some_and(|argument| {
self.is_trusted_destination(
argument,
deferred,
depth + 1,
&mut visited_symbols.clone(),
)
})
&& match new_expression.arguments.get(1) {
None => true,
Some(argument) => argument.as_expression().is_some_and(|base| {
self.is_trusted_destination(base, deferred, depth + 1, visited_symbols)
}),
}
}
Expression::CallExpression(call) => {
if let Some(member) = call.callee.get_inner_expression().as_member_expression() {
if member.static_property_name() == Some("createObjectURL")
&& self.is_global_reference(member.object(), "URL")
&& !self.global_was_mutated_before("URL", call.span, deferred)
{
return true;
}
if member.static_property_name() == Some("getURL")
&& member
.object()
.get_inner_expression()
.as_member_expression()
.is_some_and(|runtime| {
runtime.static_property_name() == Some("runtime")
&& self.is_global_reference(runtime.object(), "chrome")
&& !self
.global_was_mutated_before("chrome", call.span, deferred)
})
{
return true;
}
if matches!(
member.static_property_name(),
Some("toString" | "toJSON" | "trim" | "trimEnd" | "trimStart")
) {
let is_url_serialization =
matches!(member.static_property_name(), Some("toString" | "toJSON"))
&& self.is_global_url_instance_expression(member.object());
if is_url_serialization
&& (self.global_was_mutated_before("URL", call.span, deferred)
|| self.foreign_url_receiver_was_mutated_before(
member.object(),
call.span,
deferred,
))
{
return false;
}
return if is_url_serialization {
self.with_url_construction_allowed(|| {
self.is_trusted_destination(
member.object(),
deferred,
depth + 1,
visited_symbols,
)
})
} else {
self.is_trusted_destination(
member.object(),
deferred,
depth + 1,
visited_symbols,
)
};
}
}
let Expression::Identifier(identifier) = call.callee.get_inner_expression() else {
return false;
};
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if self.symbol_was_mutated_before(symbol_id, call.span, deferred) {
return false;
}
let Some(function_id) = self.function_id_for_symbol(symbol_id) else {
return false;
};
let Some(returns) = self.function_returns(function_id) else {
return false;
};
!returns.is_empty()
&& returns.iter().all(|returned| {
self.is_trusted_local_return(
returned,
function_id,
call,
depth + 1,
&mut visited_symbols.clone(),
)
})
}
expression if expression.as_member_expression().is_some() => {
let Some(member) = expression.as_member_expression() else {
return false;
};
if self.is_same_origin_location_read(expression) {
return true;
}
if member.static_property_name() == Some("href")
&& !member.is_computed()
&& matches!(
member.object().get_inner_expression(),
Expression::Identifier(_)
)
&& self.is_direct_global_url_instance_identifier(member.object())
&& !self.global_was_mutated_before("URL", expression.span(), deferred)
&& !self.foreign_url_receiver_was_mutated_before(
member.object(),
expression.span(),
deferred,
)
{
return self.with_url_construction_allowed(|| {
self.is_trusted_destination(
member.object(),
deferred,
depth + 1,
visited_symbols,
)
});
}
if member.is_computed() {
return self.is_trusted_const_array_index_read(
member,
deferred,
depth + 1,
visited_symbols,
);
}
member.static_property_name().is_some_and(|property_name| {
self.is_trusted_const_config_member(
member,
property_name,
deferred,
depth + 1,
visited_symbols,
)
})
}
Expression::NullLiteral(_) => true,
Expression::UnaryExpression(unary) => unary.operator == UnaryOperator::Void,
_ => false,
}
}
fn with_url_construction_allowed(&self, operation: impl FnOnce() -> bool) -> bool {
let previous = self.allow_url_construction.replace(true);
let verdict = operation();
self.allow_url_construction.set(previous);
verdict
}
fn is_global_url_instance_expression(&self, expression: &Expression<'ast>) -> bool {
match expression.get_inner_expression() {
Expression::NewExpression(new_expression) => {
self.is_global_reference(&new_expression.callee, "URL")
}
Expression::Identifier(identifier) => self
.reference_symbol(identifier)
.and_then(|symbol_id| self.symbol_const_initializer(symbol_id))
.is_some_and(|initializer| self.is_global_url_instance_expression(initializer)),
_ => false,
}
}
fn is_direct_global_url_instance_identifier(&self, expression: &Expression<'ast>) -> bool {
let Expression::Identifier(identifier) = expression.get_inner_expression() else {
return false;
};
self.reference_symbol(identifier)
.and_then(|symbol_id| self.symbol_const_initializer(symbol_id))
.is_some_and(|initializer| {
matches!(
initializer.get_inner_expression(),
Expression::NewExpression(new_expression)
if self.is_global_reference(&new_expression.callee, "URL")
)
})
}
fn foreign_url_receiver_was_mutated_before(
&self,
receiver: &Expression<'ast>,
reference_span: Span,
deferred: bool,
) -> bool {
const ORIGIN_PROPERTIES: [&str; 9] = [
"href", "host", "hostname", "password", "port", "protocol", "toJSON", "toString",
"username",
];
let Some(receiver_symbol) =
self.foreign_const_root_symbol(receiver, &mut FxHashSet::default())
else {
return false;
};
self.semantic.nodes().iter().any(|node| {
if !self.foreign_mutation_executes_before(node, reference_span, deferred) {
return false;
}
let member = match node.kind() {
AstKind::AssignmentExpression(assignment) => assignment.left.as_member_expression(),
AstKind::UpdateExpression(update) => update.argument.as_member_expression(),
AstKind::UnaryExpression(unary) if unary.operator == UnaryOperator::Delete => {
unary.argument.as_member_expression()
}
_ => None,
};
if member.is_some_and(|member| {
member
.static_property_name()
.is_none_or(|name| ORIGIN_PROPERTIES.contains(&name))
&& self.foreign_const_root_symbol(member.object(), &mut FxHashSet::default())
== Some(receiver_symbol)
}) {
return true;
}
let AstKind::CallExpression(call) = node.kind() else {
return false;
};
let Some(method_name) = self.foreign_global_mutation_method_name(call) else {
return false;
};
let target_matches = call
.arguments
.first()
.and_then(Argument::as_expression)
.and_then(|target| {
self.foreign_const_root_symbol(target, &mut FxHashSet::default())
})
== Some(receiver_symbol);
target_matches
&& ORIGIN_PROPERTIES.iter().any(|property_name| {
Self::foreign_mutation_call_may_write_property(call, method_name, property_name)
})
})
}
fn foreign_const_root_symbol(
&self,
expression: &Expression<'ast>,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> Option<SymbolId> {
let expression = expression.get_inner_expression();
if let Some(member) = expression.as_member_expression() {
return self.foreign_const_root_symbol(member.object(), visited_symbols);
}
let Expression::Identifier(identifier) = expression else {
return None;
};
let symbol_id = self.reference_symbol(identifier)?;
self.foreign_const_root_symbol_id(symbol_id, visited_symbols)
}
fn foreign_const_root_symbol_id(
&self,
symbol_id: SymbolId,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> Option<SymbolId> {
if !visited_symbols.insert(symbol_id) {
return Some(symbol_id);
}
let Some(Expression::Identifier(alias)) = self
.symbol_const_initializer(symbol_id)
.map(Expression::get_inner_expression)
else {
return Some(symbol_id);
};
self.reference_symbol(alias)
.and_then(|alias_symbol| {
self.foreign_const_root_symbol_id(alias_symbol, visited_symbols)
})
.or(Some(symbol_id))
}
fn foreign_mutation_call_may_write_property(
call: &CallExpression<'ast>,
method_name: &str,
property_name: &str,
) -> bool {
match method_name {
"defineProperty" | "set" => call
.arguments
.get(1)
.and_then(Argument::as_expression)
.and_then(WindowOpenAnalysis::static_mutation_property_name)
.is_none_or(|name| name == property_name),
"assign" => call.arguments.iter().skip(1).any(|argument| {
let Some(Expression::ObjectExpression(object)) = argument
.as_expression()
.map(Expression::get_inner_expression)
else {
return true;
};
object.properties.iter().any(|property| match property {
ObjectPropertyKind::ObjectProperty(property) => property
.key
.static_name()
.is_none_or(|name| name.as_ref() == property_name),
ObjectPropertyKind::SpreadProperty(_) => true,
})
}),
"defineProperties" => {
let Some(Expression::ObjectExpression(object)) = call
.arguments
.get(1)
.and_then(Argument::as_expression)
.map(Expression::get_inner_expression)
else {
return true;
};
object.properties.iter().any(|property| match property {
ObjectPropertyKind::ObjectProperty(property) => property
.key
.static_name()
.is_none_or(|name| name.as_ref() == property_name),
ObjectPropertyKind::SpreadProperty(_) => true,
})
}
"setPrototypeOf" => true,
_ => false,
}
}
fn is_trusted_const_config_member(
&self,
member: &oxc_ast::ast::MemberExpression<'ast>,
property_name: &str,
deferred: bool,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let Expression::Identifier(receiver) = member.object().get_inner_expression() else {
return false;
};
let Some(symbol_id) = self.reference_symbol(receiver) else {
return false;
};
if self.symbol_was_mutated_before(symbol_id, member.span(), deferred)
|| self.foreign_receiver_was_mutated_before(
member.object(),
Some(property_name),
member.span(),
deferred,
)
{
return false;
}
let Some(Expression::ObjectExpression(object)) = self
.symbol_const_initializer(symbol_id)
.map(Expression::get_inner_expression)
else {
return false;
};
self.object_supplies_trusted_property(
object,
property_name,
deferred,
depth,
visited_symbols,
)
}
fn is_trusted_const_array_index_read(
&self,
member: &oxc_ast::ast::MemberExpression<'ast>,
deferred: bool,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let oxc_ast::ast::MemberExpression::ComputedMemberExpression(computed) = member else {
return false;
};
let Expression::NumericLiteral(index) = computed.expression.get_inner_expression() else {
return false;
};
if index.value < 0.0 || index.value.fract() != 0.0 {
return false;
}
let Expression::Identifier(receiver) = computed.object.get_inner_expression() else {
return false;
};
let Some(symbol_id) = self.reference_symbol(receiver) else {
return false;
};
if self.symbol_was_mutated_before(symbol_id, member.span(), deferred)
|| self.foreign_receiver_was_mutated_before(
&computed.object,
None,
member.span(),
deferred,
)
{
return false;
}
let Some(initializer) = self.symbol_const_initializer(symbol_id) else {
return false;
};
let element_is_trusted = |array: &oxc_ast::ast::ArrayExpression<'ast>| {
array
.elements
.get(index.value as usize)
.and_then(ArrayExpressionElement::as_expression)
.is_some_and(|element| {
self.is_trusted_destination(
element,
deferred,
depth + 1,
&mut visited_symbols.clone(),
)
})
};
match initializer.get_inner_expression() {
Expression::ArrayExpression(array) => element_is_trusted(array),
Expression::CallExpression(call) => {
let Expression::Identifier(identifier) = call.callee.get_inner_expression() else {
return false;
};
let Some(function_symbol) = self.reference_symbol(identifier) else {
return false;
};
if self.symbol_was_mutated_before(function_symbol, call.span, deferred) {
return false;
}
let Some(function_id) = self.function_id_for_symbol(function_symbol) else {
return false;
};
let Some(returns) = self.function_returns(function_id) else {
return false;
};
!returns.is_empty()
&& returns.iter().all(|returned| {
let Expression::ArrayExpression(array) = returned.get_inner_expression()
else {
return false;
};
element_is_trusted(array)
})
}
_ => false,
}
}
fn object_supplies_trusted_property(
&self,
object: &oxc_ast::ast::ObjectExpression<'ast>,
property_name: &str,
deferred: bool,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let mut trusted_value = None;
for property in &object.properties {
match property {
ObjectPropertyKind::ObjectProperty(property)
if property.key.static_name().as_deref() == Some(property_name) =>
{
trusted_value = Some(self.is_trusted_destination(
&property.value,
deferred,
depth + 1,
&mut visited_symbols.clone(),
));
}
ObjectPropertyKind::SpreadProperty(_) => trusted_value = Some(false),
ObjectPropertyKind::ObjectProperty(property)
if property.computed && property.key.static_name().is_none() =>
{
trusted_value = Some(false);
}
_ => {}
}
}
trusted_value == Some(true)
}
fn foreign_receiver_was_mutated_before(
&self,
receiver: &Expression<'ast>,
property_name: Option<&str>,
reference_span: Span,
deferred: bool,
) -> bool {
const MUTATING_ARRAY_METHODS: [&str; 9] = [
"copyWithin",
"fill",
"pop",
"push",
"reverse",
"shift",
"sort",
"splice",
"unshift",
];
let Some(receiver_symbol) =
self.foreign_const_root_symbol(receiver, &mut FxHashSet::default())
else {
return false;
};
self.semantic.nodes().iter().any(|node| {
if !self.foreign_mutation_executes_before(node, reference_span, deferred) {
return false;
}
let member = match node.kind() {
AstKind::AssignmentExpression(assignment) => assignment.left.as_member_expression(),
AstKind::UpdateExpression(update) => update.argument.as_member_expression(),
AstKind::UnaryExpression(unary) if unary.operator == UnaryOperator::Delete => {
unary.argument.as_member_expression()
}
_ => None,
};
if member.is_some_and(|member| {
self.foreign_const_root_symbol(member.object(), &mut FxHashSet::default())
== Some(receiver_symbol)
&& property_name.is_none_or(|property_name| {
member
.static_property_name()
.is_none_or(|name| name == property_name)
})
}) {
return true;
}
let AstKind::CallExpression(call) = node.kind() else {
return false;
};
if let Some(callee_member) = call.callee.as_member_expression()
&& callee_member
.static_property_name()
.is_some_and(|name| MUTATING_ARRAY_METHODS.contains(&name))
&& self.foreign_const_root_symbol(callee_member.object(), &mut FxHashSet::default())
== Some(receiver_symbol)
{
return true;
}
let Some(method_name) = self.foreign_global_mutation_method_name(call) else {
return false;
};
let target_matches = call
.arguments
.first()
.and_then(Argument::as_expression)
.and_then(|target| {
self.foreign_const_root_symbol(target, &mut FxHashSet::default())
})
== Some(receiver_symbol);
target_matches
&& property_name.is_some_and(|property_name| {
Self::foreign_mutation_call_may_write_property(call, method_name, property_name)
})
})
}
fn is_trusted_foreign_interpolated_prefix(text: &str) -> bool {
let text = text.trim_start_matches(is_ecmascript_whitespace);
if text.is_empty() {
return false;
}
let lower = text.to_ascii_lowercase();
["mailto:", "tel:", "sms:", "file:"]
.iter()
.any(|scheme| lower.starts_with(scheme))
|| WindowOpenAnalysis::starts_unambiguous_same_origin_path(text)
}
fn is_trusted_concat_prefix(
&self,
expression: &Expression<'ast>,
deferred: bool,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
if depth > MAX_RESOLUTION_DEPTH {
return false;
}
match expression.get_inner_expression() {
Expression::StringLiteral(literal) => {
Self::is_trusted_foreign_interpolated_prefix(literal.value.as_str())
}
Expression::TemplateLiteral(template) => {
if template.expressions.is_empty() {
return template.quasis.first().is_some_and(|quasi| {
Self::is_trusted_foreign_interpolated_prefix(quasi.value.raw.as_str())
});
}
self.is_trusted_destination(expression, deferred, depth + 1, visited_symbols)
}
Expression::Identifier(identifier) => {
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if !visited_symbols.insert(symbol_id)
|| self.symbol_was_mutated_before(symbol_id, expression.span(), deferred)
{
return false;
}
self.symbol_const_initializer(symbol_id)
.is_some_and(|initializer| {
self.is_trusted_concat_prefix(
initializer,
deferred,
depth + 1,
visited_symbols,
)
})
}
_ => false,
}
}
fn static_primitive_truthiness(&self, expression: &Expression<'ast>) -> Option<bool> {
if self.is_nullish(expression) {
return Some(false);
}
match expression.get_inner_expression() {
Expression::BooleanLiteral(literal) => Some(literal.value),
Expression::StringLiteral(literal) => Some(!literal.value.is_empty()),
Expression::NumericLiteral(literal) => {
Some(literal.value != 0.0 && !literal.value.is_nan())
}
Expression::BigIntLiteral(literal) => Some(!literal.is_zero()),
_ => None,
}
}
fn is_proven_safe_slash_base(
&self,
expression: &Expression<'ast>,
deferred: bool,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
if depth > MAX_RESOLUTION_DEPTH {
return false;
}
match expression.get_inner_expression() {
Expression::StringLiteral(literal) => {
let without_line_whitespace: String = literal
.value
.chars()
.filter(|character| !matches!(character, '\t' | '\n' | '\r'))
.collect();
let normalized = without_line_whitespace
.trim_matches(|character| character <= ' ')
.replace('\\', "/");
let normalized_lowercase = normalized.to_ascii_lowercase();
let is_incomplete_scheme =
["http:", "https:", "ftp:", "ws:", "wss:"]
.iter()
.any(|prefix| {
normalized_lowercase
.strip_prefix(prefix)
.is_some_and(|suffix| {
suffix.chars().all(|character| character == '/')
})
});
!normalized.is_empty()
&& normalized.chars().any(|character| character != '/')
&& !is_incomplete_scheme
}
Expression::BooleanLiteral(_)
| Expression::NumericLiteral(_)
| Expression::BigIntLiteral(_)
| Expression::NullLiteral(_)
| Expression::UnaryExpression(_) => {
self.static_primitive_truthiness(expression).is_some()
}
Expression::ConditionalExpression(conditional) => {
if let Some(test_truthiness) = self.static_primitive_truthiness(&conditional.test) {
return self.is_proven_safe_slash_base(
if test_truthiness {
&conditional.consequent
} else {
&conditional.alternate
},
deferred,
depth + 1,
visited_symbols,
);
}
self.is_proven_safe_slash_base(
&conditional.consequent,
deferred,
depth + 1,
&mut visited_symbols.clone(),
) && self.is_proven_safe_slash_base(
&conditional.alternate,
deferred,
depth + 1,
&mut visited_symbols.clone(),
)
}
Expression::LogicalExpression(logical) => {
let left_truthiness = self.static_primitive_truthiness(&logical.left);
match logical.operator {
LogicalOperator::And => {
left_truthiness == Some(false)
|| self.is_proven_safe_slash_base(
&logical.right,
deferred,
depth + 1,
visited_symbols,
)
}
LogicalOperator::Coalesce if self.is_nullish(&logical.left) => self
.is_proven_safe_slash_base(
&logical.right,
deferred,
depth + 1,
visited_symbols,
),
LogicalOperator::Coalesce if left_truthiness.is_some() => self
.is_proven_safe_slash_base(
&logical.left,
deferred,
depth + 1,
visited_symbols,
),
LogicalOperator::Or if left_truthiness.is_some() => self
.is_proven_safe_slash_base(
if left_truthiness == Some(true) {
&logical.left
} else {
&logical.right
},
deferred,
depth + 1,
visited_symbols,
),
_ => {
self.is_proven_safe_slash_base(
&logical.left,
deferred,
depth + 1,
&mut visited_symbols.clone(),
) && self.is_proven_safe_slash_base(
&logical.right,
deferred,
depth + 1,
&mut visited_symbols.clone(),
)
}
}
}
Expression::Identifier(identifier) => {
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if !visited_symbols.insert(symbol_id)
|| self.symbol_was_mutated_before(symbol_id, expression.span(), deferred)
{
return false;
}
self.symbol_const_initializer(symbol_id)
.is_some_and(|initializer| {
self.is_proven_safe_slash_base(
initializer,
deferred,
depth + 1,
visited_symbols,
)
})
}
Expression::CallExpression(call) => {
let Expression::Identifier(identifier) = call.callee.get_inner_expression() else {
return false;
};
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if self.symbol_was_mutated_before(symbol_id, call.span, deferred) {
return false;
}
let Some(function_id) = self.function_id_for_symbol(symbol_id) else {
return false;
};
let Some(returns) = self.function_returns(function_id) else {
return false;
};
!returns.is_empty()
&& returns.iter().all(|returned| {
self.is_proven_safe_slash_base(
returned,
true,
depth + 1,
&mut visited_symbols.clone(),
)
})
}
expression => self.is_same_origin_location_read(expression),
}
}
fn is_trusted_interpolated_base(
&self,
expression: &Expression<'ast>,
deferred: bool,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let expression = expression.get_inner_expression();
if !matches!(expression, Expression::StringLiteral(_))
&& self.static_primitive_truthiness(expression).is_some()
{
return true;
}
match expression {
Expression::ConditionalExpression(conditional) => {
if let Some(test_truthiness) = self.static_primitive_truthiness(&conditional.test) {
return self.is_trusted_interpolated_base(
if test_truthiness {
&conditional.consequent
} else {
&conditional.alternate
},
deferred,
depth + 1,
visited_symbols,
);
}
self.is_trusted_interpolated_base(
&conditional.consequent,
deferred,
depth + 1,
&mut visited_symbols.clone(),
) && self.is_trusted_interpolated_base(
&conditional.alternate,
deferred,
depth + 1,
&mut visited_symbols.clone(),
)
}
Expression::LogicalExpression(logical) => {
let left_truthiness = self.static_primitive_truthiness(&logical.left);
if logical.operator == LogicalOperator::And && left_truthiness == Some(false) {
return true;
}
if logical.operator == LogicalOperator::Coalesce && self.is_nullish(&logical.left) {
return self.is_trusted_interpolated_base(
&logical.right,
deferred,
depth + 1,
visited_symbols,
);
}
if let Some(left_truthiness) = left_truthiness {
let branch = if logical.operator == LogicalOperator::Or && !left_truthiness {
&logical.right
} else {
&logical.left
};
return self.is_trusted_interpolated_base(
branch,
deferred,
depth + 1,
visited_symbols,
);
}
self.is_trusted_interpolated_base(
&logical.left,
deferred,
depth + 1,
&mut visited_symbols.clone(),
) && self.is_trusted_interpolated_base(
&logical.right,
deferred,
depth + 1,
&mut visited_symbols.clone(),
)
}
_ => self.is_trusted_destination(expression, deferred, depth + 1, visited_symbols),
}
}
fn is_statically_truthy_trusted(
&self,
expression: &Expression<'ast>,
deferred: bool,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
if depth > MAX_RESOLUTION_DEPTH {
return false;
}
match expression.get_inner_expression() {
Expression::StringLiteral(literal) => {
!literal.value.is_empty()
&& WindowOpenAnalysis::is_trusted_foreign_static_text(literal.value.as_str())
}
Expression::TemplateLiteral(template) => {
template
.quasis
.iter()
.any(|quasi| !quasi.value.raw.is_empty())
&& self.is_trusted_destination(expression, deferred, depth + 1, visited_symbols)
}
Expression::Identifier(identifier) => {
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if !visited_symbols.insert(symbol_id) {
return false;
}
self.symbol_const_initializer(symbol_id)
.is_some_and(|initializer| {
self.is_statically_truthy_trusted(
initializer,
deferred,
depth + 1,
visited_symbols,
)
})
}
_ => false,
}
}
fn is_trusted_local_return(
&self,
returned: &Expression<'ast>,
function_id: NodeId,
call: &CallExpression<'ast>,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
if let Expression::Identifier(identifier) = returned.get_inner_expression()
&& let Some(symbol_id) = self.reference_symbol(identifier)
&& let Some(parameter_index) = self.function_parameter_index(function_id, symbol_id)
&& let Some(argument) = call
.arguments
.get(parameter_index)
.and_then(Argument::as_expression)
{
return self.is_trusted_destination(argument, true, depth + 1, visited_symbols);
}
if let Expression::BinaryExpression(binary) = returned.get_inner_expression()
&& binary.operator == oxc_syntax::operator::BinaryOperator::Addition
{
let mut operands = Vec::new();
WindowOpenAnalysis::flatten_concat_operands(returned, &mut operands);
if let Some(Expression::Identifier(first_identifier)) = operands
.first()
.map(|operand| operand.get_inner_expression())
&& let Some(symbol_id) = self.reference_symbol(first_identifier)
&& let Some(parameter_index) = self.function_parameter_index(function_id, symbol_id)
&& let Some(argument) = call
.arguments
.get(parameter_index)
.and_then(Argument::as_expression)
{
let mut suffix = String::new();
let mut following_expression = false;
for operand in operands.iter().skip(1) {
let Some(text) = WindowOpenAnalysis::static_string_text(operand) else {
following_expression = true;
break;
};
suffix.push_str(text);
}
if WindowOpenAnalysis::is_safe_interpolated_suffix(&suffix, following_expression)
&& (!suffix.starts_with('/')
|| self.is_proven_safe_slash_base(
argument,
true,
depth + 1,
&mut visited_symbols.clone(),
))
{
return self.is_trusted_destination(argument, true, depth + 1, visited_symbols);
}
}
}
if let Expression::TemplateLiteral(template) = returned.get_inner_expression()
&& template
.quasis
.first()
.is_none_or(|quasi| quasi.value.raw.is_empty())
&& let Some(Expression::Identifier(first_identifier)) = template
.expressions
.first()
.map(Expression::get_inner_expression)
&& let Some(symbol_id) = self.reference_symbol(first_identifier)
&& let Some(parameter_index) = self.function_parameter_index(function_id, symbol_id)
&& let Some(argument) = call
.arguments
.get(parameter_index)
.and_then(Argument::as_expression)
{
let suffix = template
.quasis
.get(1)
.map_or("", |quasi| quasi.value.raw.as_str());
if WindowOpenAnalysis::is_safe_interpolated_suffix(
suffix,
template.expressions.len() > 1,
) && (!suffix.starts_with('/')
|| self.is_proven_safe_slash_base(
argument,
true,
depth + 1,
&mut visited_symbols.clone(),
))
{
return self.is_trusted_destination(argument, true, depth + 1, visited_symbols);
}
}
self.is_trusted_destination(returned, true, depth, visited_symbols)
}
fn is_trusted_or_nullish(
&self,
expression: &Expression<'ast>,
deferred: bool,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
self.is_nullish(expression)
|| self.is_trusted_destination(expression, deferred, depth, visited_symbols)
}
fn is_nullish(&self, expression: &Expression<'ast>) -> bool {
match expression.get_inner_expression() {
Expression::NullLiteral(_) => true,
Expression::UnaryExpression(unary) => unary.operator == UnaryOperator::Void,
Expression::Identifier(identifier) if identifier.name == "undefined" => {
self.reference_symbol(identifier).is_none()
}
_ => false,
}
}
fn is_same_origin_location_read(&self, expression: &Expression<'ast>) -> bool {
let Some(member) = expression.as_member_expression() else {
return false;
};
if member.is_computed() {
return false;
}
let Some(property_name) = member.static_property_name() else {
return false;
};
if !matches!(property_name, "origin" | "href") {
return false;
}
self.is_location_receiver(member.object(), &mut FxHashSet::default())
|| (property_name == "origin" && self.is_global_reference(member.object(), "window"))
}
fn is_location_receiver(
&self,
expression: &Expression<'ast>,
visited_functions: &mut FxHashSet<NodeId>,
) -> bool {
if self.is_global_reference(expression, "location") {
return true;
}
let Expression::CallExpression(call) = expression.get_inner_expression() else {
return false;
};
let Expression::Identifier(identifier) = call.callee.get_inner_expression() else {
return false;
};
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if self.symbol_was_mutated_before(symbol_id, call.span, true) {
return false;
}
let Some(function_id) = self.function_id_for_symbol(symbol_id) else {
return false;
};
if !visited_functions.insert(function_id) {
return false;
}
let Some(returns) = self.function_returns(function_id) else {
return false;
};
!returns.is_empty()
&& returns
.iter()
.all(|returned| self.is_location_receiver(returned, &mut visited_functions.clone()))
}
fn is_global_reference(&self, expression: &Expression<'ast>, name: &str) -> bool {
self.is_global_reference_inner(expression, name, &mut FxHashSet::default())
}
fn is_global_reference_inner(
&self,
expression: &Expression<'ast>,
name: &str,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let Expression::Identifier(identifier) = expression.get_inner_expression() else {
return expression.as_member_expression().is_some_and(|member| {
member.static_property_name() == Some(name)
&& matches!(member.object().get_inner_expression(), Expression::Identifier(object)
if self.reference_symbol(object).is_none()
&& matches!(object.name.as_str(), "global" | "globalThis" | "window" | "self"))
});
};
let Some(symbol_id) = self.reference_symbol(identifier) else {
return identifier.name == name;
};
if !visited_symbols.insert(symbol_id) {
return false;
}
let declaration = self.semantic.symbol_declaration(symbol_id);
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return false;
};
if !matches!(
self.semantic.nodes().parent_node(declaration.id()).kind(),
AstKind::VariableDeclaration(variable) if variable.kind.is_const()
) {
return false;
}
let Some(initializer) = &declarator.init else {
return false;
};
if binding_property_name_for_symbol(&declarator.id, symbol_id).as_deref() == Some(name)
&& ["global", "globalThis", "self", "window"]
.iter()
.any(|receiver_name| {
self.is_global_reference_inner(
initializer,
receiver_name,
&mut visited_symbols.clone(),
)
})
{
return true;
}
self.is_global_reference_inner(initializer, name, visited_symbols)
}
fn global_was_mutated_before(&self, name: &str, reference_span: Span, deferred: bool) -> bool {
self.semantic.nodes().iter().any(|node| {
let target_matches = match node.kind() {
AstKind::AssignmentExpression(assignment) => match &assignment.left {
oxc_ast::ast::AssignmentTarget::AssignmentTargetIdentifier(identifier) => {
identifier.name == name
&& self
.semantic
.scoping()
.get_reference(identifier.reference_id())
.symbol_id()
.is_none()
}
target => target.as_member_expression().is_some_and(|member| {
self.foreign_member_chain_starts_at_global(
member,
name,
&mut FxHashSet::default(),
)
}),
},
AstKind::UpdateExpression(update) => update
.argument
.as_member_expression()
.is_some_and(|member| {
self.foreign_member_chain_starts_at_global(
member,
name,
&mut FxHashSet::default(),
)
}),
AstKind::UnaryExpression(unary) if unary.operator == UnaryOperator::Delete => {
unary.argument.as_member_expression().is_some_and(|member| {
self.foreign_member_chain_starts_at_global(
member,
name,
&mut FxHashSet::default(),
)
})
}
AstKind::CallExpression(call)
if self.foreign_global_mutation_method_name(call).is_some() =>
{
call.arguments
.first()
.and_then(Argument::as_expression)
.is_some_and(|target| {
self.foreign_expression_targets_global(
target,
name,
&mut FxHashSet::default(),
)
})
}
_ => false,
};
if !target_matches {
return false;
}
self.foreign_mutation_executes_before(node, reference_span, deferred)
})
}
fn foreign_expression_targets_global(
&self,
expression: &Expression<'ast>,
name: &str,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
if self.is_global_reference(expression, name) {
return true;
}
if let Some(member) = expression.get_inner_expression().as_member_expression() {
return self.foreign_member_chain_starts_at_global(member, name, visited_symbols);
}
let Expression::Identifier(identifier) = expression.get_inner_expression() else {
return false;
};
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if !visited_symbols.insert(symbol_id) {
return false;
}
let declaration = self.semantic.symbol_declaration(symbol_id);
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return false;
};
if binding_property_name_for_symbol(&declarator.id, symbol_id).as_deref()
== Some("prototype")
&& declarator
.init
.as_ref()
.is_some_and(|initializer| self.is_global_reference(initializer, name))
{
return true;
}
declarator.init.as_ref().is_some_and(|initializer| {
self.foreign_expression_targets_global(initializer, name, visited_symbols)
})
}
fn foreign_member_chain_starts_at_global(
&self,
member: &oxc_ast::ast::MemberExpression<'ast>,
name: &str,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
self.foreign_expression_targets_global(member.object(), name, visited_symbols)
}
fn foreign_global_mutation_method_name<'call>(
&self,
call: &'call CallExpression<'ast>,
) -> Option<&'call str> {
const OBJECT_METHODS: [&str; 4] = [
"assign",
"defineProperties",
"defineProperty",
"setPrototypeOf",
];
const REFLECT_METHODS: [&str; 3] = ["defineProperty", "set", "setPrototypeOf"];
let callee = call.callee.get_inner_expression();
let method_name = WindowOpenAnalysis::terminal_callee_name(callee)?;
if let Some(member) = callee.as_member_expression() {
let is_object_method = OBJECT_METHODS.contains(&method_name)
&& self.is_global_reference(member.object(), "Object");
let is_reflect_method = REFLECT_METHODS.contains(&method_name)
&& self.is_global_reference(member.object(), "Reflect");
return (is_object_method || is_reflect_method).then_some(method_name);
}
let Expression::Identifier(identifier) = callee else {
return None;
};
let is_object_method = OBJECT_METHODS.contains(&method_name)
&& self.foreign_identifier_resolves_global_method(
identifier,
"Object",
method_name,
&mut FxHashSet::default(),
);
let is_reflect_method = REFLECT_METHODS.contains(&method_name)
&& self.foreign_identifier_resolves_global_method(
identifier,
"Reflect",
method_name,
&mut FxHashSet::default(),
);
(is_object_method || is_reflect_method).then_some(method_name)
}
fn foreign_identifier_resolves_global_method(
&self,
identifier: &oxc_ast::ast::IdentifierReference<'ast>,
namespace_name: &str,
method_name: &str,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let Some(symbol_id) = self.reference_symbol(identifier) else {
return false;
};
if !visited_symbols.insert(symbol_id) {
return false;
}
let declaration = self.semantic.symbol_declaration(symbol_id);
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return false;
};
if !matches!(
self.semantic.nodes().parent_node(declaration.id()).kind(),
AstKind::VariableDeclaration(variable) if variable.kind.is_const()
) {
return false;
}
let Some(initializer) = &declarator.init else {
return false;
};
if binding_property_name_for_symbol(&declarator.id, symbol_id).as_deref()
== Some(method_name)
&& self.is_global_reference(initializer, namespace_name)
{
return true;
}
match initializer.get_inner_expression() {
Expression::Identifier(alias) => self.foreign_identifier_resolves_global_method(
alias,
namespace_name,
method_name,
visited_symbols,
),
expression => expression.as_member_expression().is_some_and(|member| {
member.static_property_name() == Some(method_name)
&& self.is_global_reference(member.object(), namespace_name)
}),
}
}
fn symbol_was_mutated_before(
&self,
symbol_id: SymbolId,
reference_span: Span,
deferred: bool,
) -> bool {
let reference_function = self
.node_for_span(reference_span)
.and_then(|node| self.nearest_function(node.id()));
self.semantic
.scoping()
.get_resolved_references(symbol_id)
.any(|reference| {
if reference.is_read() {
return false;
}
let node = self.semantic.nodes().get_node(reference.node_id());
let write_function = self.nearest_function(node.id());
(write_function == reference_function && node.span().start < reference_span.start)
|| (write_function != reference_function
&& self.foreign_mutation_executes_before(node, reference_span, deferred))
})
}
fn foreign_mutation_executes_before(
&self,
mutation_node: &AstNode<'ast>,
reference_span: Span,
deferred: bool,
) -> bool {
let reference_function = self
.node_for_span(reference_span)
.and_then(|node| self.nearest_function(node.id()));
let mutation_function = self.nearest_function(mutation_node.id());
if mutation_function == reference_function {
return mutation_node.span().start < reference_span.start;
}
match mutation_function {
None => deferred,
Some(function_id) => self.function_executes_during_module_init(
function_id,
reference_span,
deferred,
&mut FxHashSet::default(),
),
}
}
fn function_executes_during_module_init(
&self,
function_id: NodeId,
reference_span: Span,
deferred: bool,
visited_functions: &mut FxHashSet<NodeId>,
) -> bool {
if !visited_functions.insert(function_id) {
return false;
}
let function_node = self.semantic.nodes().get_node(function_id);
let symbol_id = match function_node.kind() {
AstKind::Function(function) => function
.id
.as_ref()
.map(|identifier| identifier.symbol_id()),
AstKind::ArrowFunctionExpression(_) => {
let parent = self.semantic.nodes().parent_node(function_id);
match parent.kind() {
AstKind::VariableDeclarator(declarator) => declarator
.id
.get_binding_identifier()
.map(|identifier| identifier.symbol_id()),
_ => None,
}
}
_ => None,
};
let Some(symbol_id) = symbol_id else {
return false;
};
self.semantic
.scoping()
.get_resolved_references(symbol_id)
.any(|reference| {
let reference_node = self.semantic.nodes().get_node(reference.node_id());
let parent = self.semantic.nodes().parent_node(reference_node.id());
if !matches!(parent.kind(), AstKind::CallExpression(call)
if call.callee.span() == reference_node.span())
{
return false;
}
self.nearest_function(parent.id()).map_or_else(
|| deferred || parent.span().start < reference_span.start,
|owner| {
self.function_executes_during_module_init(
owner,
reference_span,
deferred,
visited_functions,
)
},
)
})
}
fn symbol_initializer(&self, symbol_id: SymbolId) -> Option<&Expression<'ast>> {
let declaration = self.semantic.symbol_declaration(symbol_id);
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return None;
};
declarator.init.as_ref()
}
fn symbol_const_initializer(&self, symbol_id: SymbolId) -> Option<&Expression<'ast>> {
let declaration = self.semantic.symbol_declaration(symbol_id);
if !matches!(
self.semantic.nodes().parent_node(declaration.id()).kind(),
AstKind::VariableDeclaration(variable) if variable.kind.is_const()
) {
return None;
}
self.symbol_initializer(symbol_id)
}
fn is_foreign_let_assigned_only_trusted(
&self,
symbol_id: SymbolId,
reference_span: Span,
deferred: bool,
depth: usize,
visited_symbols: &mut FxHashSet<SymbolId>,
) -> bool {
let declaration = self.semantic.symbol_declaration(symbol_id);
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return false;
};
if declarator
.id
.get_binding_identifier()
.is_none_or(|identifier| identifier.symbol_id() != symbol_id)
{
return false;
}
let mut saw_trusted_value = false;
if let Some(initializer) = &declarator.init {
saw_trusted_value = true;
if !self.is_trusted_destination(
initializer,
deferred,
depth + 1,
&mut visited_symbols.clone(),
) {
return false;
}
}
for reference in self.semantic.scoping().get_resolved_references(symbol_id) {
if reference.is_read() {
continue;
}
let reference_node = self.semantic.nodes().get_node(reference.node_id());
if !self.foreign_mutation_executes_before(reference_node, reference_span, deferred) {
continue;
}
let parent = self.semantic.nodes().parent_node(reference_node.id());
let AstKind::AssignmentExpression(assignment) = parent.kind() else {
return false;
};
if assignment.operator != AssignmentOperator::Assign
|| assignment.left.span() != reference_node.span()
{
return false;
}
saw_trusted_value = true;
if !self.is_trusted_destination(
&assignment.right,
deferred,
depth + 1,
&mut visited_symbols.clone(),
) {
return false;
}
}
saw_trusted_value
}
fn function_id_for_symbol(&self, symbol_id: SymbolId) -> Option<NodeId> {
let declaration = self.semantic.symbol_declaration(symbol_id);
match declaration.kind() {
AstKind::Function(function) if function.body.is_some() => Some(function.node_id.get()),
AstKind::VariableDeclarator(declarator) => {
if !matches!(
self.semantic.nodes().parent_node(declaration.id()).kind(),
AstKind::VariableDeclaration(variable) if variable.kind.is_const()
) {
return None;
}
match declarator.init.as_ref()?.get_inner_expression() {
Expression::ArrowFunctionExpression(function) => Some(function.node_id.get()),
Expression::FunctionExpression(function) => Some(function.node_id.get()),
_ => None,
}
}
_ => None,
}
}
fn function_returns(&self, function_id: NodeId) -> Option<Vec<&Expression<'ast>>> {
let function_node = self.semantic.nodes().get_node(function_id);
if let AstKind::ArrowFunctionExpression(arrow) = function_node.kind()
&& !matches!(&arrow.body, ArrowFunctionBody::FunctionBody(_))
{
return Some(vec![arrow.body.to_expression()]);
}
let mut returned_expressions = Vec::new();
for node in self.semantic.nodes().iter() {
let AstKind::ReturnStatement(statement) = node.kind() else {
continue;
};
if self.nearest_function(node.id()) != Some(function_id) {
continue;
}
let Some(argument) = statement.argument.as_ref() else {
return None;
};
returned_expressions.push(argument);
}
Some(returned_expressions)
}
fn function_parameter_index(&self, function_id: NodeId, symbol_id: SymbolId) -> Option<usize> {
let function_node = self.semantic.nodes().get_node(function_id);
let parameters = match function_node.kind() {
AstKind::Function(function) => &function.params.items,
AstKind::ArrowFunctionExpression(arrow) => &arrow.params.items,
_ => return None,
};
parameters.iter().position(|parameter| {
parameter
.pattern
.get_binding_identifier()
.is_some_and(|identifier| identifier.symbol_id() == symbol_id)
})
}
fn reference_symbol(
&self,
identifier: &oxc_ast::ast::IdentifierReference<'ast>,
) -> Option<SymbolId> {
self.semantic
.scoping()
.get_reference(identifier.reference_id())
.symbol_id()
}
fn nearest_function(&self, node_id: NodeId) -> Option<NodeId> {
self.semantic
.nodes()
.ancestors(node_id)
.find_map(|ancestor| {
matches!(
ancestor.kind(),
AstKind::Function(_) | AstKind::ArrowFunctionExpression(_)
)
.then_some(ancestor.id())
})
}
fn node_for_span(&self, span: Span) -> Option<&AstNode<'ast>> {
self.semantic
.nodes()
.iter()
.find(|node| node.span() == span)
}
}