use skif_core::{HashMap, HashSet};
use std::cell::{Cell, RefCell};
use std::path::{Component, Path, PathBuf};
use std::rc::Rc;
use std::sync::LazyLock;
use oxc_allocator::Allocator;
use oxc_ast::ast::{
Argument, ArrayExpressionElement, ArrowFunctionExpression, AssignmentOperator, AssignmentTarget,
AssignmentTargetPropertyIdentifier, BinaryExpression, BinaryOperator, BindingIdentifier, BindingPattern,
BlockStatement, CallExpression, CatchClause, ChainElement, ChainExpression, ComputedMemberExpression,
ConditionalExpression, Declaration, ExportDefaultDeclarationKind, Expression, ExpressionStatement, ForInStatement,
ForOfStatement, ForStatement, Function, IdentifierReference, IfStatement, ImportDeclarationSpecifier,
ImportOrExportKind, LogicalExpression, LogicalOperator, MemberExpression, NewExpression, ObjectProperty,
ObjectPropertyKind, Program, ReturnStatement, SimpleAssignmentTarget, Statement, StaticMemberExpression,
TaggedTemplateExpression, TemplateLiteral, UnaryExpression, UnaryOperator, VariableDeclaration,
VariableDeclarationKind, VariableDeclarator,
};
use oxc_ast_visit::{Visit, walk};
use oxc_parser::{ParseOptions as OxcParseOptions, Parser};
use oxc_span::{SourceType, Span};
use serde::{Deserialize, Serialize};
use crate::finder::static_string;
use crate::sharing::{
Change, Imports, Unstable, Write, alias_names, alias_source, changed_by_call, given, given_parts, property_defined,
reach, statement_call,
};
const MAX_DEPTH: u32 = 32;
const MAX_CONST_CHAIN: usize = 64;
const MAX_CONST_NODES: usize = 10_000;
const MAX_CALLS: usize = 256;
const MAX_IMPORT_DEPTH: usize = 8;
const MAX_MODULE_LOADS: usize = 64;
const MAX_MODULE_ANALYSES: usize = 4 * MAX_MODULE_LOADS;
const MAX_MODULE_EVALUATIONS: usize = 1024;
const GLOB_PACKAGES: &[&str] = &["glob", "fast-glob", "globby", "tinyglobby"];
const MERGE_PACKAGES: &[&str] = &[
"lodash",
"lodash-es",
"lodash/merge",
"lodash/mergeWith",
"lodash.merge",
"lodash.mergewith",
"deepmerge",
"deepmerge-ts",
"ts-deepmerge",
"merge",
"merge-deep",
"webpack-merge",
"defu",
];
const AMBIGUOUS_MERGES: &[&str] = &["merge", "mergeWith"];
const GLOB_MEMBERS: &[&str] = &["sync", "async", "glob", "globSync", "globbySync", "stream"];
const OBJECT_PROTOTYPE_MEMBERS: &[&str] = &[
"__defineGetter__",
"__defineSetter__",
"__lookupGetter__",
"__lookupSetter__",
"__proto__",
"constructor",
"hasOwnProperty",
"isPrototypeOf",
"propertyIsEnumerable",
"toLocaleString",
"toString",
"valueOf",
];
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ConfigValue {
Null,
Bool(bool),
Number(f64),
String(String),
Array(Vec<ConfigValue>),
Object(Vec<(String, ConfigValue)>),
Regex {
pattern: String,
flags: String,
},
Unknown,
OneOf(Vec<ConfigValue>),
}
pub const UNKNOWN_SPREAD: &str = "...";
pub const OWN_SPREAD: &str = "...own";
impl ConfigValue {
#[must_use]
pub fn is_spread_key(key: &str) -> bool {
key == UNKNOWN_SPREAD || key == OWN_SPREAD
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&ConfigValue> {
match self {
Self::Object(entries) => entries.iter().rev().find(|(k, _)| k == key).map(|(_, v)| v),
_ => None,
}
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Self::String(s) => Some(s),
_ => None,
}
}
#[must_use]
pub fn as_array(&self) -> Option<&[ConfigValue]> {
match self {
Self::Array(items) => Some(items),
_ => None,
}
}
#[must_use]
pub fn as_object(&self) -> Option<&[(String, ConfigValue)]> {
match self {
Self::Object(entries) => Some(entries),
_ => None,
}
}
#[must_use]
pub fn as_regex(&self) -> Option<&str> {
match self {
Self::Regex { pattern, .. } => Some(pattern),
_ => None,
}
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(b) => Some(*b),
_ => None,
}
}
#[must_use]
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown | Self::OneOf(_))
}
#[must_use]
pub fn alternatives(&self) -> &[ConfigValue] {
match self {
Self::OneOf(alternatives) => alternatives,
other => std::slice::from_ref(other),
}
}
#[must_use]
pub fn has_unknown(&self) -> bool {
match self {
Self::Unknown | Self::OneOf(_) => true,
Self::Array(items) => items.iter().any(Self::has_unknown),
Self::Object(entries) => entries.iter().any(|(_, v)| v.has_unknown()),
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
enum Value {
Null,
Undefined,
Bool(bool),
Number(f64),
String(String),
Array(Vec<Value>),
Object(Vec<(String, Value)>),
Regex {
pattern: String,
flags: String,
},
Unknown,
OneOf(Vec<Value>),
Maybe(Box<Value>),
}
impl Value {
fn get(&self, key: &str) -> Option<&Value> {
match self {
Self::Object(entries) => entries.iter().rev().find(|(k, _)| k == key).map(|(_, v)| v),
_ => None,
}
}
fn as_str(&self) -> Option<&str> {
match self {
Self::String(s) => Some(s),
_ => None,
}
}
fn doubtful(value: Self, definite: bool) -> Self {
if definite || matches!(value, Self::Unknown | Self::Maybe(_)) {
value
} else {
Self::Maybe(Box::new(value))
}
}
fn split(self) -> (Self, bool) {
match self {
Self::Maybe(value) => (*value, false),
value => (value, true),
}
}
fn is_definite(&self) -> bool {
!matches!(self, Self::Maybe(_))
}
fn definite(&self) -> &Self {
match self {
Self::Maybe(value) => value,
value => value,
}
}
fn definite_mut(&mut self) -> &mut Self {
match self {
Self::Maybe(value) => value,
value => value,
}
}
}
impl From<ConfigValue> for Value {
fn from(value: ConfigValue) -> Self {
match value {
ConfigValue::Null => Self::Null,
ConfigValue::Bool(b) => Self::Bool(b),
ConfigValue::Number(n) => Self::Number(n),
ConfigValue::String(s) => Self::String(s),
ConfigValue::Array(items) => Self::Array(items.into_iter().map(Self::from).collect()),
ConfigValue::Object(entries) => Self::Object(
entries
.into_iter()
.map(|(key, value)| (key, Self::from(value)))
.collect(),
),
ConfigValue::Regex { pattern, flags } => Self::Regex { pattern, flags },
ConfigValue::Unknown => Self::Unknown,
ConfigValue::OneOf(alternatives) => Self::OneOf(alternatives.into_iter().map(Self::from).collect()),
}
}
}
impl From<Value> for ConfigValue {
fn from(value: Value) -> Self {
match value {
Value::Null | Value::Undefined => Self::Null,
Value::Bool(b) => Self::Bool(b),
Value::Number(n) => Self::Number(n),
Value::String(s) => Self::String(s),
Value::Array(items) => Self::Array(items.into_iter().map(Self::from).collect()),
Value::Object(entries) => Self::Object(
entries
.into_iter()
.map(|(key, value)| (key, Self::from(value)))
.collect(),
),
Value::Regex { pattern, flags } => Self::Regex { pattern, flags },
Value::Unknown => Self::Unknown,
Value::OneOf(alternatives) => {
let mut out: Vec<Self> = Vec::new();
for alternative in alternatives.into_iter().map(Self::from) {
if !out.contains(&alternative) {
out.push(alternative);
}
}
match out.len() {
1 => out.pop().unwrap_or(Self::Unknown),
_ => Self::OneOf(out),
}
}
Value::Maybe(value) => Self::from(*value),
}
}
}
pub trait ModuleLoader {
fn load(&self, from: &Path, specifier: &str) -> Option<(PathBuf, String)>;
fn is_installed(&self, _from: &Path, _specifier: &str) -> bool {
false
}
fn holds_no_code(&self, _from: &Path, _specifier: &str) -> bool {
false
}
}
#[must_use]
pub fn extract_config_object(source: &str, path: &Path) -> Option<ConfigValue> {
evaluate_config(source, path, None).map(|config| config.value)
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ExtractedConfig {
pub value: ConfigValue,
pub call_arguments: Vec<ConfigValue>,
}
#[must_use]
pub fn extract_config(source: &str, path: &Path, loader: Option<&dyn ModuleLoader>) -> Option<ExtractedConfig> {
let Some(loader) = loader else {
return evaluate_config(source, path, None);
};
let modules = Modules {
loader,
root: path.parent().map_or_default(Path::to_path_buf),
active: RefCell::new(vec![(path.to_path_buf(), Imported::Default)]),
loads: RefCell::new(HashSet::default()),
evaluations: Cell::new(0),
cuts: Cell::new(0),
values: RefCell::new(HashMap::default()),
loaded: RefCell::new(HashMap::default()),
changing: RefCell::new(HashMap::default()),
pending: RefCell::new(Vec::new()),
shallowest_cut: Cell::new(usize::MAX),
analyses: Cell::new(0),
installed: RefCell::new(HashMap::default()),
};
evaluate_config(source, path, Some(&modules))
}
#[must_use]
pub fn extract_config_object_with(source: &str, path: &Path, loader: &dyn ModuleLoader) -> Option<ConfigValue> {
extract_config(source, path, Some(loader)).map(|config| config.value)
}
fn evaluate_config(source: &str, path: &Path, modules: Option<&Modules<'_>>) -> Option<ExtractedConfig> {
crate::with_arena(|allocator| {
let program = parse_program(allocator, source, path)?;
let followed = modules.map(|modules| FollowedImports { modules, from: path });
let module_imports: &dyn Imports = match &followed {
Some(followed) => followed,
None => &NoImports,
};
let top = top_level(&program, module_imports);
let bindings = module_bindings(&program);
let imports = import_bindings(&program);
let file = path.file_name().and_then(|name| name.to_str()).unwrap_or_default();
let eval = Evaluator::new(&top, &bindings, &imports, file, ".", path, modules);
let exported = exported(&program)?;
let value = eval.exported_value(exported, exports_changed(&program));
let call_arguments = eval.exported_call_arguments(exported, &value);
Some(ExtractedConfig {
value: ConfigValue::from(value),
call_arguments: call_arguments.into_iter().map(ConfigValue::from).collect(),
})
})
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Imported {
Default,
Named(String),
Namespace,
}
impl Imported {
fn named(name: String) -> Self {
if name == "default" {
Self::Default
} else {
Self::Named(name)
}
}
}
struct ImportBinding {
specifier: String,
imported: Imported,
}
fn import_bindings(program: &Program<'_>) -> HashMap<String, ImportBinding> {
let mut imports = HashMap::default();
for stmt in &program.body {
let Statement::ImportDeclaration(d) = stmt else {
continue;
};
if d.import_kind.is_type() {
continue;
}
let specifier = crate::js_string(d.source.value);
for item in d.specifiers.iter().flatten() {
let (local, imported) = match item {
ImportDeclarationSpecifier::ImportSpecifier(s) if s.import_kind == ImportOrExportKind::Value => (
s.local.name.as_str(),
Imported::named(crate::collect::module_export_name(&s.imported)),
),
ImportDeclarationSpecifier::ImportSpecifier(_) => continue,
ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => (s.local.name.as_str(), Imported::Default),
ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => (s.local.name.as_str(), Imported::Namespace),
};
imports.insert(
local.to_owned(),
ImportBinding {
specifier: specifier.clone(),
imported,
},
);
}
}
imports
}
enum ExportSource {
Local(String),
From(String, Imported),
}
fn export_table(program: &Program<'_>) -> (HashMap<String, ExportSource>, Vec<String>) {
let mut table = HashMap::default();
let mut stars = Vec::new();
for stmt in &program.body {
match stmt {
Statement::ExportDeclaration(e) => match &e.declaration {
Declaration::VariableDeclaration(v) => {
for d in &v.declarations {
if let BindingPattern::BindingIdentifier(id) = &d.id {
let name = id.name.as_str().to_owned();
table.insert(name.clone(), ExportSource::Local(name));
}
}
}
Declaration::FunctionDeclaration(f) => {
if let Some(id) = &f.id {
let name = id.name.as_str().to_owned();
table.insert(name.clone(), ExportSource::Local(name));
}
}
_ => {}
},
Statement::ExportNamedDeclaration(e) if !e.export_kind.is_type() => {
for s in e.specifiers.iter().filter(|s| !s.export_kind.is_type()) {
table.insert(
crate::collect::module_export_name(&s.exported),
ExportSource::Local(crate::collect::module_export_name(&s.local)),
);
}
}
Statement::ExportFromDeclaration(e) if !e.export_kind.is_type() => {
let from = crate::js_string(e.source.value);
for s in e.specifiers.iter().filter(|s| !s.export_kind.is_type()) {
table.insert(
crate::collect::module_export_name(&s.exported),
ExportSource::From(
from.clone(),
Imported::named(crate::collect::module_export_name(&s.local)),
),
);
}
}
Statement::ExportAllDeclaration(e) if !e.export_kind.is_type() => {
let from = crate::js_string(e.source.value);
match &e.exported {
Some(name) => {
table.insert(
crate::collect::module_export_name(name),
ExportSource::From(from, Imported::Namespace),
);
}
None => stars.push(from),
}
}
_ => {}
}
}
(table, stars)
}
type LoadedModule = (PathBuf, Rc<str>);
struct Modules<'l> {
loader: &'l dyn ModuleLoader,
root: PathBuf,
active: RefCell<Vec<(PathBuf, Imported)>>,
loads: RefCell<HashSet<PathBuf>>,
evaluations: Cell<usize>,
cuts: Cell<usize>,
values: RefCell<HashMap<(PathBuf, Imported), Option<Value>>>,
loaded: RefCell<HashMap<(PathBuf, String), Option<LoadedModule>>>,
changing: RefCell<HashMap<PathBuf, Option<Rc<ModuleFacts>>>>,
pending: RefCell<Vec<PathBuf>>,
shallowest_cut: Cell<usize>,
analyses: Cell<usize>,
installed: RefCell<HashMap<(PathBuf, String), bool>>,
}
impl Modules<'_> {
fn load(&self, from: &Path, specifier: &str) -> Option<LoadedModule> {
let key = (from.parent().unwrap_or(from).to_path_buf(), specifier.to_owned());
if let Some(known) = self.loaded.borrow().get(&key) {
return known.clone();
}
let module = self
.loader
.load(from, specifier)
.map(|(path, source)| (path, Rc::from(source)));
self.loaded.borrow_mut().insert(key, module.clone());
module
}
fn follows(&self, from: &Path, specifier: &str) -> bool {
self.load(from, specifier).is_some()
}
fn is_installed(&self, from: &Path, specifier: &str) -> bool {
let key = (from.parent().unwrap_or(from).to_path_buf(), specifier.to_owned());
if let Some(&known) = self.installed.borrow().get(&key) {
return known;
}
let installed = self.loader.is_installed(from, specifier);
self.installed.borrow_mut().insert(key, installed);
installed
}
fn changes_modules(&self, from: &Path, specifier: &str) -> bool {
self.facts(from, specifier).is_some_and(|(_, facts)| facts.changes)
}
fn result_places(&self, from: &Path, specifier: &str, export: &str) -> Option<Vec<Write>> {
match self.export_result_places(from, specifier, export, 0) {
ExportResult::Places(places) => Some(places),
ExportResult::Missing | ExportResult::Unknown => None,
}
}
fn export_result_places(&self, from: &Path, specifier: &str, export: &str, depth: usize) -> ExportResult {
if depth >= MAX_IMPORT_DEPTH {
return ExportResult::Unknown;
}
let Some((path, facts)) = self.facts(from, specifier) else {
return ExportResult::Missing;
};
let local = match facts.exports.get(export) {
Some(ExportSource::Local(local)) => Some(local.as_str()),
Some(ExportSource::From(next, imported)) => {
let name = match imported {
Imported::Default => "default",
Imported::Named(name) => name.as_str(),
Imported::Namespace => return ExportResult::Unknown,
};
return match self.export_result_places(&path, next, name, depth + 1) {
ExportResult::Missing => ExportResult::Unknown,
found => found,
};
}
None if export == "default" => facts.default.as_deref(),
None => {
return facts
.stars
.iter()
.map(|next| self.export_result_places(&path, next, export, depth + 1))
.find(|found| !matches!(found, ExportResult::Missing))
.unwrap_or(ExportResult::Missing);
}
};
local
.and_then(|local| facts.results.get(local).cloned())
.map_or(ExportResult::Unknown, ExportResult::Places)
}
fn facts(&self, from: &Path, specifier: &str) -> Option<(PathBuf, Rc<ModuleFacts>)> {
let (path, source) = self.load(from, specifier)?;
if let Some(known) = self.changing.borrow().get(&path) {
let facts = known.clone().unwrap_or_else(|| {
let at = self.pending.borrow().iter().position(|pending| *pending == path);
self.shallowest_cut.set(self.shallowest_cut.get().min(at.unwrap_or(0)));
Rc::new(ModuleFacts::unknown())
});
return Some((path, facts));
}
let depth = self.pending.borrow().len();
if depth >= MAX_IMPORT_DEPTH
|| self.changing.borrow().len() >= MAX_MODULE_LOADS
|| self.analyses.get() >= MAX_MODULE_ANALYSES
{
self.shallowest_cut.set(0);
return Some((path, Rc::new(ModuleFacts::unknown())));
}
self.analyses.set(self.analyses.get() + 1);
self.changing.borrow_mut().insert(path.clone(), None);
self.pending.borrow_mut().push(path.clone());
let outer = self.shallowest_cut.replace(usize::MAX);
let facts = Rc::new(self.module_facts(&path, &source));
self.pending.borrow_mut().pop();
let cut = self.shallowest_cut.get();
if cut < depth {
self.changing.borrow_mut().remove(&path);
self.shallowest_cut.set(outer.min(cut));
} else {
self.changing.borrow_mut().insert(path.clone(), Some(Rc::clone(&facts)));
self.shallowest_cut.set(outer);
}
Some((path, facts))
}
fn module_facts(&self, path: &Path, source: &str) -> ModuleFacts {
let Ok(source_type) = SourceType::from_path(path) else {
return ModuleFacts::unknown();
};
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source, source_type).parse();
if ret.diagnostics.errors().next().is_some() {
return ModuleFacts::unknown();
}
let unstable = Unstable::of(
&ret.program,
&FollowedImports {
modules: self,
from: path,
},
);
let changes = unstable.changes_modules()
|| unstable
.exposed_modules()
.iter()
.any(|specifier| self.changes_modules(path, specifier))
|| self.reexports_change_modules(path, &ret.program);
let (exports, stars) = export_table(&ret.program);
let default = exported(&ret.program).and_then(|exported| match exported {
Exported::Function(function) => function.id.as_ref().map(|id| id.name.as_str().to_owned()),
Exported::Expression(expr) => match expr.get_inner_expression() {
Expression::Identifier(id) => Some(id.name.as_str().to_owned()),
_ => None,
},
});
ModuleFacts {
changes,
results: unstable.result_places().clone(),
exports,
stars,
default,
}
}
fn reexports_change_modules(&self, path: &Path, program: &Program<'_>) -> bool {
reexported(program).into_iter().any(|specifier| {
if self.follows(path, &specifier) {
self.changes_modules(path, &specifier)
} else {
!self.is_installed(path, &specifier) && !self.loader.holds_no_code(path, &specifier)
}
})
}
fn evaluate(&self, from: &Path, specifier: &str, imported: &Imported, args: Option<&[Value]>) -> Option<Value> {
let (path, source) = self.load(from, specifier)?;
let key = (path.clone(), imported.clone());
if args.is_none()
&& let Some(known) = self.values.borrow().get(&key)
{
return known.clone();
}
if self.cut(&key) {
self.cuts.set(self.cuts.get() + 1);
return Some(Value::Unknown);
}
self.loads.borrow_mut().insert(path.clone());
self.evaluations.set(self.evaluations.get() + 1);
let cuts = self.cuts.get();
self.active.borrow_mut().push(key.clone());
let value = self.evaluate_module(&path, &source, imported, args).map(as_imported);
self.active.borrow_mut().pop();
if args.is_none() && self.cuts.get() == cuts {
self.values.borrow_mut().insert(key, value.clone());
}
value
}
fn cut(&self, key: &(PathBuf, Imported)) -> bool {
let active = self.active.borrow();
let loads = self.loads.borrow();
active.contains(key)
|| active.len() >= MAX_IMPORT_DEPTH
|| (!loads.contains(&key.0) && loads.len() >= MAX_MODULE_LOADS)
|| self.evaluations.get() >= MAX_MODULE_EVALUATIONS
}
fn evaluate_module(&self, path: &Path, source: &str, imported: &Imported, args: Option<&[Value]>) -> Option<Value> {
let source_type = SourceType::from_path(path).ok()?;
let allocator = Allocator::default();
let ret = Parser::new(&allocator, source, source_type)
.with_options(OxcParseOptions {
preserve_parens: false,
..OxcParseOptions::default()
})
.parse();
if ret.diagnostics.errors().next().is_some() {
return Some(Value::Unknown);
}
let program = &ret.program;
let top = top_level(
program,
&FollowedImports {
modules: self,
from: path,
},
);
let bindings = module_bindings(program);
let imports = import_bindings(program);
let dir = relative_dir(&self.root, path.parent().unwrap_or(path));
let file = path.file_name().and_then(|name| name.to_str()).unwrap_or_default();
let eval = Evaluator::new(&top, &bindings, &imports, file, &dir, path, Some(self));
match imported {
Imported::Namespace => Some(Value::Unknown),
Imported::Default => Some(match exported(program)? {
Exported::Function(function) => eval.invoke(Callable::Function(function), args, true, 0),
Exported::Expression(expr) => {
match (args, Callable::of_expression(expr), expr.get_inner_expression()) {
(None, _, _) => eval.expr(expr, 0),
(Some(args), Some(callable), _) => eval.invoke(callable, Some(args), true, 0),
(Some(args), None, Expression::Identifier(id)) => eval.call_named(id.name.as_str(), args),
(Some(_), None, _) => Value::Unknown,
}
}
})
.map(|value| {
if exports_changed(program) {
changed(value)
} else {
value
}
}),
Imported::Named(name) => {
let (table, stars) = export_table(program);
match table.get(name) {
Some(ExportSource::Local(local)) => Some(match args {
None => eval.identifier(local, 0),
Some(args) => eval.call_named(local, args),
}),
Some(ExportSource::From(from, imported)) => self.evaluate(path, from, imported, args),
None => stars.iter().find_map(|from| self.evaluate(path, from, imported, args)),
}
}
}
}
}
fn reexported(program: &Program<'_>) -> Vec<String> {
program
.body
.iter()
.filter_map(|stmt| match stmt {
Statement::ExportFromDeclaration(e) if !e.export_kind.is_type() => Some(&e.source),
Statement::ExportAllDeclaration(e) if !e.export_kind.is_type() => Some(&e.source),
_ => None,
})
.map(|source| crate::js_string(source.value))
.collect()
}
enum ExportResult {
Missing,
Unknown,
Places(Vec<Write>),
}
struct ModuleFacts {
changes: bool,
results: HashMap<String, Vec<Write>>,
exports: HashMap<String, ExportSource>,
stars: Vec<String>,
default: Option<String>,
}
impl ModuleFacts {
fn unknown() -> Self {
Self {
changes: true,
results: HashMap::default(),
exports: HashMap::default(),
stars: Vec::new(),
default: None,
}
}
}
fn as_imported(value: Value) -> Value {
match value {
Value::Object(entries) => Value::Object(
entries
.into_iter()
.map(|(key, value)| {
let key = if key == OWN_SPREAD {
UNKNOWN_SPREAD.to_owned()
} else {
key
};
(key, as_imported(value))
})
.collect(),
),
Value::Array(items) => Value::Array(items.into_iter().map(as_imported).collect()),
Value::OneOf(alternatives) => Value::OneOf(alternatives.into_iter().map(as_imported).collect()),
Value::Maybe(value) => Value::Maybe(Box::new(as_imported(*value))),
other => other,
}
}
fn relative_dir(from: &Path, to: &Path) -> String {
let from: Vec<Component<'_>> = from.components().collect();
let to: Vec<Component<'_>> = to.components().collect();
let common = from.iter().zip(&to).take_while(|(a, b)| a == b).count();
let mut parts: Vec<String> = vec!["..".to_owned(); from.len() - common];
parts.extend(
to[common..]
.iter()
.map(|part| part.as_os_str().to_string_lossy().into_owned()),
);
let parts: Vec<&str> = parts.iter().map(String::as_str).collect();
normalized(&parts)
}
fn first_parameter(callable: Callable<'_, '_>) -> Option<String> {
let params = match callable {
Callable::Function(f) => &f.params,
Callable::Arrow(f) => &f.params,
};
match ¶ms.items.first()?.pattern {
BindingPattern::BindingIdentifier(id) => Some(id.name.as_str().to_owned()),
_ => None,
}
}
struct TopLevel<'p, 'a> {
consts: HashMap<String, &'p Expression<'a>>,
functions: HashMap<String, Callable<'p, 'a>>,
changed: HashMap<String, TopChange>,
unstable: Unstable,
}
#[derive(Clone, Copy)]
enum Callable<'p, 'a> {
Function(&'p Function<'a>),
Arrow(&'p ArrowFunctionExpression<'a>),
}
impl<'p, 'a> Callable<'p, 'a> {
fn same(self, other: Self) -> bool {
match (self, other) {
(Self::Function(a), Self::Function(b)) => std::ptr::eq(a, b),
(Self::Arrow(a), Self::Arrow(b)) => std::ptr::eq(a, b),
_ => false,
}
}
fn of_expression(expr: &'p Expression<'a>) -> Option<Self> {
match expr.get_inner_expression() {
Expression::ArrowFunctionExpression(f) => Some(Self::Arrow(f)),
Expression::FunctionExpression(f) => Some(Self::Function(f)),
_ => None,
}
}
}
#[derive(Clone, Copy)]
enum LocalBinding<'p, 'a> {
Function(Callable<'p, 'a>),
Other,
}
#[derive(Clone)]
enum Binding<'p, 'a> {
Value(Value),
Callable(Callable<'p, 'a>),
Alias(Alias),
}
#[derive(Clone)]
struct Alias {
value: Value,
root: String,
frame: usize,
keys: Vec<String>,
}
impl Alias {
fn same_part(&self, other: &Self) -> bool {
self.root == other.root && self.frame == other.frame && self.keys == other.keys
}
}
enum Edit {
Set(String, Value),
Insert { front: bool, items: Vec<Value> },
Taint,
}
enum TopChange {
Assigned,
Written(Vec<Write>),
}
type Frame<'p, 'a> = HashMap<String, Binding<'p, 'a>>;
struct NoImports;
impl Imports for NoImports {
fn follows(&self, _: &str) -> bool {
false
}
fn changes_modules(&self, _: &str) -> bool {
false
}
fn is_installed(&self, _: &str) -> bool {
true
}
}
struct FollowedImports<'m, 'l> {
modules: &'m Modules<'l>,
from: &'m Path,
}
impl Imports for FollowedImports<'_, '_> {
fn follows(&self, specifier: &str) -> bool {
self.modules.follows(self.from, specifier)
}
fn changes_modules(&self, specifier: &str) -> bool {
self.modules.changes_modules(self.from, specifier)
}
fn result_places(&self, specifier: &str, export: &str) -> Option<Vec<Write>> {
self.modules.result_places(self.from, specifier, export)
}
fn is_installed(&self, specifier: &str) -> bool {
self.modules.is_installed(self.from, specifier)
}
}
fn top_level<'p, 'a>(program: &'p Program<'a>, imports: &dyn Imports) -> TopLevel<'p, 'a> {
let unstable = Unstable::of(program, imports);
let mut top = TopLevel {
consts: HashMap::default(),
functions: HashMap::default(),
changed: HashMap::default(),
unstable: Unstable::default(),
};
for stmt in &program.body {
let decl = match stmt {
Statement::ExportDeclaration(e) => Some(&e.declaration),
other => other.as_declaration(),
};
let writes = match stmt {
Statement::ExpressionStatement(s) => Some(Writes::of_effect(&s.expression, &unstable)),
Statement::IfStatement(_)
| Statement::BlockStatement(_)
| Statement::ForStatement(_)
| Statement::ForInStatement(_)
| Statement::ForOfStatement(_)
| Statement::WhileStatement(_)
| Statement::DoWhileStatement(_)
| Statement::SwitchStatement(_)
| Statement::TryStatement(_)
| Statement::LabeledStatement(_)
| Statement::WithStatement(_)
| Statement::VariableDeclaration(_)
| Statement::ExportNamedDeclaration(_)
| Statement::ExportDefaultDeclaration(_) => Some(Writes::of_statement(stmt, &unstable)),
_ => None,
};
if let Some(writes) = writes {
top.record(writes);
}
match decl {
Some(Declaration::VariableDeclaration(v)) if v.kind == VariableDeclarationKind::Const => {
for d in &v.declarations {
if let (BindingPattern::BindingIdentifier(id), Some(init)) = (&d.id, &d.init) {
top.consts.insert(id.name.as_str().to_string(), init);
if let Some(callable) = Callable::of_expression(init) {
top.functions.insert(id.name.as_str().to_string(), callable);
}
}
}
}
Some(Declaration::FunctionDeclaration(f)) => {
if let Some(id) = &f.id {
top.functions
.insert(id.name.as_str().to_string(), Callable::Function(f));
}
}
_ => {}
}
}
top.unstable = unstable;
top
}
const ARRAY_CALLBACK_METHODS: &[&str] = &["map", "flatMap", "filter"];
impl TopLevel<'_, '_> {
fn record(&mut self, writes: Writes<'_>) {
for name in writes.assigned {
self.changed.insert(name, TopChange::Assigned);
}
for (name, write) in writes.changed {
if let TopChange::Written(list) = self
.changed
.entry(name)
.or_insert_with(|| TopChange::Written(Vec::new()))
{
list.push(write);
}
}
}
}
const PATH_FUNCTIONS: &[&str] = &["resolve", "join", "dirname", "normalize"];
#[derive(Default)]
struct ModuleBindings {
path_modules: HashSet<String>,
path_functions: HashMap<String, String>,
url_modules: HashSet<String>,
file_url_to_path: HashSet<String>,
glob_names: HashSet<String>,
merge_names: HashSet<String>,
foreign_merges: HashSet<String>,
flat_config_functions: HashSet<String>,
flat_config_modules: HashSet<String>,
}
impl ModuleBindings {
fn bind(&mut self, module: &str, member: Option<&str>, local: &str) {
let local = local.to_owned();
if MERGE_PACKAGES.contains(&module) {
self.merge_names.insert(local.clone());
} else if AMBIGUOUS_MERGES.contains(&local.as_str()) {
self.foreign_merges.insert(local.clone());
}
match (module.strip_prefix("node:").unwrap_or(module), member) {
("path" | "path/posix", None | Some("default")) => {
self.path_modules.insert(local);
}
("path" | "path/posix", Some(name)) if PATH_FUNCTIONS.contains(&name) => {
self.path_functions.insert(local, name.to_owned());
}
("url", None | Some("default")) => {
self.url_modules.insert(local);
}
("url", Some("fileURLToPath")) => {
self.file_url_to_path.insert(local);
}
(package, _) if GLOB_PACKAGES.contains(&package) => {
self.glob_names.insert(local);
}
("fs", Some("globSync")) | ("fs/promises", Some("glob")) => {
self.glob_names.insert(local);
}
("eslint/config", Some("defineConfig")) | ("typescript-eslint", Some("config")) => {
self.flat_config_functions.insert(local);
}
("typescript-eslint", None | Some("default")) => {
self.flat_config_modules.insert(local);
}
_ => {}
}
}
}
fn module_bindings(program: &Program<'_>) -> ModuleBindings {
let mut bindings = ModuleBindings::default();
for stmt in &program.body {
if let Statement::ImportDeclaration(d) = stmt {
let module = d.source.value.as_str().unwrap_or_default();
for specifier in d.specifiers.iter().flatten() {
match specifier {
ImportDeclarationSpecifier::ImportSpecifier(s) => {
let imported = crate::collect::module_export_name(&s.imported);
bindings.bind(module, Some(&imported), s.local.name.as_str());
}
ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
bindings.bind(module, None, s.local.name.as_str());
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
bindings.bind(module, None, s.local.name.as_str());
}
}
}
} else if let Some(Declaration::VariableDeclaration(v)) = stmt.as_declaration() {
for declarator in &v.declarations {
if let Some(module) = declarator.init.as_ref().and_then(required_module) {
bind_required(&mut bindings, &module, &declarator.id);
} else if let Some(Expression::Identifier(source)) =
declarator.init.as_ref().map(Expression::get_inner_expression)
{
let module = if bindings.path_modules.contains(source.name.as_str()) {
Some("path")
} else if bindings.url_modules.contains(source.name.as_str()) {
Some("url")
} else {
None
};
if let Some(module) = module {
bind_required(&mut bindings, module, &declarator.id);
}
}
}
}
}
bindings
}
fn required_module(init: &Expression<'_>) -> Option<String> {
let Expression::CallExpression(call) = init.get_inner_expression() else {
return None;
};
match &call.callee {
Expression::Identifier(id) if id.name.as_str() == "require" => call
.arguments
.first()
.and_then(Argument::as_expression)
.and_then(static_string),
_ => None,
}
}
fn bind_required(bindings: &mut ModuleBindings, module: &str, id: &BindingPattern<'_>) {
match id {
BindingPattern::BindingIdentifier(local) => bindings.bind(module, None, local.name.as_str()),
BindingPattern::ObjectPattern(pattern) => {
for property in &pattern.properties {
if let (Some(key), BindingPattern::BindingIdentifier(local)) =
(property.key.static_name(), &property.value)
{
bindings.bind(module, Some(&crate::js_string(key.as_js_str())), local.name.as_str());
}
}
}
_ => {}
}
}
fn normalized(parts: &[&str]) -> String {
let mut segments: Vec<&str> = Vec::new();
for part in parts.iter().flat_map(|part| part.split(['/', '\\'])) {
match part {
"" | "." => {}
".." if segments.last().is_some_and(|last| *last != "..") => {
segments.pop();
}
other => segments.push(other),
}
}
match segments.first() {
None => ".".to_owned(),
Some(&"..") => segments.join("/"),
Some(_) => format!("./{}", segments.join("/")),
}
}
fn apply_path_function(function: &str, args: &[Value]) -> Value {
let Some(parts) = args.iter().map(Value::as_str).collect::<Option<Vec<&str>>>() else {
return Value::Unknown;
};
let absolute = |part: &&str| part.starts_with(['/', '\\']);
let path = match (function, parts.as_slice()) {
("fileURLToPath", [file]) if !file.contains(':') => (*file).to_owned(),
("dirname", [path]) => normalized(&[*path, ".."]),
("normalize", [path]) => normalized(&[*path]),
("resolve", parts) if !parts.iter().any(absolute) => normalized(parts),
("join", parts) if !parts.first().is_some_and(absolute) => normalized(parts),
_ => return Value::Unknown,
};
Value::String(path)
}
#[must_use]
pub fn extract_constructor_options(source: &str, path: &Path, constructor: &str, key: &str) -> Vec<ConfigValue> {
let Ok(source_type) = SourceType::from_path(path) else {
return Vec::new();
};
crate::with_arena(|allocator| {
let ret = Parser::new(allocator, source, source_type)
.with_options(OxcParseOptions {
preserve_parens: false,
..OxcParseOptions::default()
})
.parse();
if ret.diagnostics.errors().next().is_some() {
return Vec::new();
}
let program = &ret.program;
let top = top_level(program, &NoImports);
let bindings = module_bindings(program);
let mut finder = OptionFinder {
top: &top,
bindings: &bindings,
file: path.file_name().and_then(|name| name.to_str()).unwrap_or_default(),
constructor,
key,
found: Vec::new(),
};
finder.visit_program(program);
finder.found.into_iter().map(ConfigValue::from).collect()
})
}
#[must_use]
pub fn extract_property_values(source: &str, path: &Path, key: &str) -> Vec<ConfigValue> {
find_properties(source, path, key, None)
}
#[must_use]
pub fn extract_property_calls(source: &str, path: &Path, key: &str, argument: &ConfigValue) -> Vec<ConfigValue> {
find_properties(source, path, key, Some(argument))
}
fn find_properties(source: &str, path: &Path, key: &str, call_with: Option<&ConfigValue>) -> Vec<ConfigValue> {
crate::with_arena(|allocator| {
let Some(program) = parse_program(allocator, source, path) else {
return Vec::new();
};
let top = top_level(&program, &NoImports);
let bindings = module_bindings(&program);
let mut finder = PropertyFinder {
top: &top,
bindings: &bindings,
file: path.file_name().and_then(|name| name.to_str()).unwrap_or_default(),
key,
call_with,
evaluated: HashMap::default(),
shadowed: Vec::new(),
found: Vec::new(),
};
finder.visit_program(&program);
finder.found.into_iter().map(ConfigValue::from).collect()
})
}
#[must_use]
pub fn extract_method_calls(source: &str, path: &Path, method: &str) -> Vec<Vec<ConfigValue>> {
crate::with_arena(|allocator| {
let Some(program) = parse_program(allocator, source, path) else {
return Vec::new();
};
let top = top_level(&program, &NoImports);
let bindings = module_bindings(&program);
let mut finder = MethodCallFinder {
top: &top,
bindings: &bindings,
file: path.file_name().and_then(|name| name.to_str()).unwrap_or_default(),
method,
found: Vec::new(),
};
finder.visit_program(&program);
finder
.found
.into_iter()
.map(|arguments| arguments.into_iter().map(ConfigValue::from).collect())
.collect()
})
}
fn parse_program<'a>(allocator: &'a Allocator, source: &'a str, path: &Path) -> Option<Program<'a>> {
let source_type = SourceType::from_path(path).ok()?;
let ret = Parser::new(allocator, source, source_type)
.with_options(OxcParseOptions {
preserve_parens: false,
..OxcParseOptions::default()
})
.parse();
if ret.diagnostics.errors().next().is_some() {
return None;
}
Some(ret.program)
}
#[derive(Debug, Clone, PartialEq)]
pub struct ImportedCall {
pub module: String,
pub export: String,
pub arguments: Vec<ConfigValue>,
pub template: Option<String>,
}
pub const TEMPLATE_SUBSTITUTION: &str = "$__skif__";
pub const FILE_DIR: &str = "$__skif_file_dir__";
const PROBE_DIR: &str = "__skif__/probe";
#[must_use]
pub fn extract_imported_calls(
source: &str,
path: &Path,
modules: &[&str],
globals: &[(&str, &str)],
) -> Vec<ImportedCall> {
parse_imported_calls(source, path, modules, globals, false)
}
#[must_use]
pub fn extract_command_calls(
source: &str,
path: &Path,
modules: &[&str],
globals: &[(&str, &str)],
) -> Vec<ImportedCall> {
parse_imported_calls(source, path, modules, globals, true)
}
fn parse_imported_calls(
source: &str,
path: &Path,
modules: &[&str],
globals: &[(&str, &str)],
commands: bool,
) -> Vec<ImportedCall> {
let Ok(source_type) = SourceType::from_path(path) else {
return Vec::new();
};
crate::with_arena(|allocator| {
let ret = Parser::new(allocator, source, source_type)
.with_options(OxcParseOptions {
preserve_parens: false,
..OxcParseOptions::default()
})
.parse();
if ret.diagnostics.errors().next().is_some() {
return Vec::new();
}
let file = path.file_name().and_then(|name| name.to_str()).unwrap_or_default();
imported_calls_in(&ret.program, file, modules, globals, commands)
})
}
pub(crate) fn imported_calls_in(
program: &Program<'_>,
file: &str,
modules: &[&str],
globals: &[(&str, &str)],
commands: bool,
) -> Vec<ImportedCall> {
let imported = imported_functions(program, modules, globals);
if imported.is_empty() {
return Vec::new();
}
let top = top_level(program, &NoImports);
let bindings = module_bindings(program);
let mut finder = CallFinder {
top: &top,
bindings: &bindings,
file,
imported: &imported,
commands,
chained_cwd: HashMap::default(),
found: Vec::new(),
};
finder.visit_program(program);
finder.found
}
#[must_use]
pub fn literal_command(arguments: &[ConfigValue]) -> Option<String> {
let is_safe = |word: &str| {
!word.is_empty()
&& word
.chars()
.all(|c| c.is_alphanumeric() || matches!(c, '_' | '@' | '.' | ',' | ':' | '/' | '=' | '+' | '~' | '-'))
};
let file = arguments.first()?.as_str().filter(|file| is_safe(file))?;
let mut words = vec![file];
if let Some(ConfigValue::Array(items)) = arguments.get(1) {
for word in items.iter().filter_map(ConfigValue::as_str) {
if !is_safe(word) {
return None;
}
words.push(word);
}
}
Some(words.join(" "))
}
#[must_use]
pub fn command_line_call(arguments: &[ConfigValue]) -> Option<String> {
let command = arguments.first().and_then(ConfigValue::as_str)?;
Some(command_in_call_dir(command.to_owned(), arguments.get(1)))
}
#[must_use]
pub fn file_call_options(arguments: &[ConfigValue]) -> Option<&ConfigValue> {
match arguments.get(1) {
Some(ConfigValue::Array(_)) => arguments.get(2),
other => other,
}
}
#[must_use]
pub fn command_in_call_dir(command: String, options: Option<&ConfigValue>) -> String {
match options.and_then(|options| options.get("cwd")) {
None => command,
Some(ConfigValue::String(dir)) if dir.starts_with(FILE_DIR) => format!("cd {dir} && {command}"),
Some(_) => format!("cd {TEMPLATE_SUBSTITUTION} && {command}"),
}
}
#[must_use]
pub fn place_file_dir(command: &str, file: &str) -> String {
let dir = file.rsplit_once('/').map_or("", |(dir, _)| dir);
let quoted = if dir.is_empty() {
".".to_owned()
} else {
format!("'{}'", dir.replace('\'', r"'\''"))
};
command.replace(FILE_DIR, "ed)
}
fn mark_cwd<'p, 'a>(options: &mut ConfigValue, expression: &'p Expression<'a>, probe: &Evaluator<'_, 'p, 'a>) {
let ConfigValue::Object(entries) = options else {
return;
};
let Some(index) = entries.iter().rposition(|(key, _)| key == "cwd") else {
return;
};
let probed = ConfigValue::from(probe.expr(expression, 0));
let cwd = std::mem::replace(&mut entries[index].1, ConfigValue::Unknown);
match marked_cwd(cwd, probed.get("cwd"), sets_undefined(expression, "cwd")) {
Some(cwd) => entries[index].1 = cwd,
None => entries.retain(|(key, _)| key != "cwd"),
}
}
const ENTRYPOINTS: &str = "entrypoints";
fn mark_entrypoints<'p, 'a>(options: &mut ConfigValue, expression: &'p Expression<'a>, probe: &Evaluator<'_, 'p, 'a>) {
let ConfigValue::Object(entries) = options else {
return;
};
let Some(index) = entries.iter().rposition(|(key, _)| key == ENTRYPOINTS) else {
return;
};
let ConfigValue::Array(items) = &mut entries[index].1 else {
return;
};
let probed = ConfigValue::from(probe.expr(expression, 0));
let Some(ConfigValue::Array(probed)) = probed.get(ENTRYPOINTS) else {
return;
};
for (item, probed) in items.iter_mut().zip(probed) {
let file_relative = probed != &*item;
if file_relative && let ConfigValue::String(path) = item {
*path = format!("{FILE_DIR}/{path}");
}
}
}
fn marked_cwd(cwd: ConfigValue, probed: Option<&ConfigValue>, undefined: bool) -> Option<ConfigValue> {
let file_relative = matches!(cwd, ConfigValue::String(_)) && probed != Some(&cwd);
let unchanged = !file_relative && matches!(cwd.as_str(), Some("." | "./"));
if unchanged || undefined {
return None;
}
match cwd {
ConfigValue::String(path)
if file_relative
&& path
.chars()
.all(|c| c.is_alphanumeric() || matches!(c, '_' | '@' | '.' | '/' | '+' | '-')) =>
{
Some(ConfigValue::String(format!("{FILE_DIR}/{path}")))
}
other => Some(other),
}
}
fn is_undefined(expression: &Expression<'_>) -> bool {
match expression.get_inner_expression() {
Expression::Identifier(id) => id.name.as_str() == "undefined",
Expression::UnaryExpression(u) => u.operator == UnaryOperator::Void,
_ => false,
}
}
fn sets_undefined(expression: &Expression<'_>, key: &str) -> bool {
let Expression::ObjectExpression(object) = expression.get_inner_expression() else {
return false;
};
let last = object.properties.iter().rev().find_map(|property| match property {
ObjectPropertyKind::ObjectProperty(p)
if p.key
.static_name()
.is_some_and(|name| crate::js_string(name.as_js_str()) == key) =>
{
Some(&p.value)
}
_ => None,
});
last.is_some_and(is_undefined)
}
type ImportedFunctions = HashMap<String, (String, Option<String>)>;
type Entries<'v> = &'v [(String, Value)];
fn imported_functions(program: &Program<'_>, modules: &[&str], globals: &[(&str, &str)]) -> ImportedFunctions {
let mut imported: ImportedFunctions = globals
.iter()
.map(|(name, module)| match name.strip_suffix(".*") {
Some(object) => (object.to_owned(), ((*module).to_owned(), None)),
None => ((*name).to_owned(), ((*module).to_owned(), Some((*name).to_owned()))),
})
.collect();
let named = |export: String| Some(export).filter(|e| e != "default");
for stmt in &program.body {
if let Statement::ImportDeclaration(d) = stmt {
let module = d.source.value.as_str().unwrap_or_default();
if !modules.contains(&module) {
continue;
}
for specifier in d.specifiers.iter().flatten() {
let (local, export) = match specifier {
ImportDeclarationSpecifier::ImportSpecifier(s) => (
s.local.name.as_str(),
named(crate::collect::module_export_name(&s.imported)),
),
ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => (s.local.name.as_str(), None),
ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => (s.local.name.as_str(), None),
};
imported.insert(local.to_owned(), (module.to_owned(), export));
}
} else if let Some(Declaration::VariableDeclaration(v)) = stmt.as_declaration() {
for declarator in &v.declarations {
if let Some(module) = declarator.init.as_ref().and_then(required_module)
&& modules.contains(&module.as_str())
{
for (local, export) in required_names(&declarator.id) {
imported.insert(local, (module.clone(), export.and_then(named)));
}
}
}
}
}
imported
}
fn required_names(id: &BindingPattern<'_>) -> Vec<(String, Option<String>)> {
match id {
BindingPattern::BindingIdentifier(local) => vec![(local.name.as_str().to_owned(), None)],
BindingPattern::ObjectPattern(pattern) => pattern
.properties
.iter()
.filter_map(|property| match (property.key.static_name(), &property.value) {
(Some(key), BindingPattern::BindingIdentifier(local)) => {
Some((local.name.as_str().to_owned(), Some(crate::js_string(key.as_js_str()))))
}
_ => None,
})
.collect(),
_ => Vec::new(),
}
}
struct CallFinder<'t, 'p, 'a> {
top: &'t TopLevel<'p, 'a>,
bindings: &'t ModuleBindings,
file: &'t str,
imported: &'t ImportedFunctions,
commands: bool,
chained_cwd: HashMap<Span, Option<ConfigValue>>,
found: Vec<ImportedCall>,
}
fn template_tag<'e, 'a>(template: &'e TaggedTemplateExpression<'a>) -> &'e Expression<'a> {
match template.tag.get_inner_expression() {
Expression::CallExpression(call) => &call.callee,
other => other,
}
}
impl<'a> CallFinder<'_, '_, 'a> {
fn target(&self, callee: &Expression<'a>) -> Option<(String, String)> {
match callee.get_inner_expression() {
Expression::Identifier(id) => self
.imported
.get(id.name.as_str())
.map(|(module, export)| (module.clone(), export.clone().unwrap_or_else(|| "default".to_owned()))),
Expression::StaticMemberExpression(member) => match member.object.get_inner_expression() {
Expression::Identifier(id) => self
.imported
.get(id.name.as_str())
.filter(|(_, export)| export.is_none())
.map(|(module, _)| (module.clone(), member.property.name.as_str().to_owned())),
_ => None,
},
_ => None,
}
}
fn chained_template<'e>(&self, mut object: &'e Expression<'a>) -> Option<&'e TaggedTemplateExpression<'a>> {
loop {
match object.get_inner_expression() {
Expression::TaggedTemplateExpression(template) => {
return self.target(template_tag(template)).is_some().then_some(&**template);
}
Expression::CallExpression(call) => match call.callee.get_inner_expression() {
Expression::StaticMemberExpression(member) => object = &member.object,
_ => return None,
},
_ => return None,
}
}
}
fn record_chained_cwd(&mut self, it: &CallExpression<'a>) {
let Expression::StaticMemberExpression(member) = it.callee.get_inner_expression() else {
return;
};
if member.property.name.as_str() != "cwd" {
return;
}
let Some(template) = self.chained_template(&member.object) else {
return;
};
let Some(directory) = it.arguments.first().and_then(Argument::as_expression) else {
return;
};
let eval = Evaluator::in_file(self.top, self.bindings, self.file, HashMap::default());
let probe = Evaluator {
dir: PROBE_DIR,
..Evaluator::in_file(self.top, self.bindings, self.file, HashMap::default())
};
let probed = ConfigValue::from(probe.expr(directory, 0));
let cwd = marked_cwd(
ConfigValue::from(eval.expr(directory, 0)),
Some(&probed),
is_undefined(directory),
);
self.chained_cwd.entry(template.span).or_insert(cwd);
}
}
impl<'a> Visit<'a> for CallFinder<'_, '_, 'a> {
fn visit_call_expression(&mut self, it: &CallExpression<'a>) {
if self.commands {
self.record_chained_cwd(it);
}
if let Some((module, export)) = self.target(&it.callee) {
let eval = Evaluator::in_file(self.top, self.bindings, self.file, HashMap::default());
let mut arguments: Vec<ConfigValue> = it
.arguments
.iter()
.map(|argument| {
argument
.as_expression()
.map_or(ConfigValue::Unknown, |e| ConfigValue::from(eval.expr(e, 0)))
})
.collect();
if self.commands
&& arguments
.iter()
.any(|argument| argument.get("cwd").is_some() || argument.get(ENTRYPOINTS).is_some())
{
let probe = Evaluator {
dir: PROBE_DIR,
..Evaluator::in_file(self.top, self.bindings, self.file, HashMap::default())
};
for (argument, expression) in arguments.iter_mut().zip(&it.arguments) {
if let Some(expression) = expression.as_expression() {
mark_entrypoints(argument, expression, &probe);
mark_cwd(argument, expression, &probe);
}
}
}
self.found.push(ImportedCall {
module,
export,
arguments,
template: None,
});
}
walk::walk_call_expression(self, it);
}
fn visit_tagged_template_expression(&mut self, it: &TaggedTemplateExpression<'a>) {
if let Some((module, export)) = self.target(template_tag(it)) {
let quasis: Vec<&str> = it.quasi.quasis.iter().map(|q| q.value.raw.as_str()).collect();
let arguments = self
.chained_cwd
.remove(&it.span)
.flatten()
.map_or_default(|cwd| vec![ConfigValue::Object(vec![("cwd".to_owned(), cwd)])]);
self.found.push(ImportedCall {
module,
export,
arguments,
template: Some(quasis.join(TEMPLATE_SUBSTITUTION)),
});
}
walk::walk_tagged_template_expression(self, it);
}
}
struct OptionFinder<'t, 'p, 'a> {
top: &'t TopLevel<'p, 'a>,
bindings: &'t ModuleBindings,
file: &'t str,
constructor: &'t str,
key: &'t str,
found: Vec<Value>,
}
impl<'a> Visit<'a> for OptionFinder<'_, '_, 'a> {
fn visit_new_expression(&mut self, it: &NewExpression<'a>) {
let name = match it.callee.get_inner_expression() {
Expression::Identifier(id) => Some(id.name.as_str()),
Expression::StaticMemberExpression(member) => Some(member.property.name.as_str()),
_ => None,
};
if name == Some(self.constructor) {
let eval = Evaluator::in_file(self.top, self.bindings, self.file, HashMap::default());
for argument in &it.arguments {
let Some(Expression::ObjectExpression(options)) =
argument.as_expression().map(Expression::get_inner_expression)
else {
continue;
};
for property in &options.properties {
if let ObjectPropertyKind::ObjectProperty(p) = property
&& !p.method
&& p.key
.static_name()
.is_some_and(|name| crate::js_string(name.as_js_str()) == self.key)
{
self.found.push(eval.expr(&p.value, 0));
}
}
}
}
walk::walk_new_expression(self, it);
}
}
struct MethodCallFinder<'t, 'p, 'a> {
top: &'t TopLevel<'p, 'a>,
bindings: &'t ModuleBindings,
file: &'t str,
method: &'t str,
found: Vec<Vec<Value>>,
}
impl<'a> Visit<'a> for MethodCallFinder<'_, '_, 'a> {
fn visit_call_expression(&mut self, it: &CallExpression<'a>) {
if let Expression::StaticMemberExpression(member) = it.callee.get_inner_expression()
&& member.property.name.as_str() == self.method
{
let eval = Evaluator::in_file(self.top, self.bindings, self.file, HashMap::default());
let arguments = it
.arguments
.iter()
.map(|argument| argument.as_expression().map_or(Value::Unknown, |e| eval.expr(e, 0)))
.collect();
self.found.push(arguments);
}
walk::walk_call_expression(self, it);
}
}
struct PropertyFinder<'t, 'p, 'a> {
top: &'t TopLevel<'p, 'a>,
bindings: &'t ModuleBindings,
file: &'t str,
key: &'t str,
call_with: Option<&'t ConfigValue>,
evaluated: HashMap<String, Value>,
shadowed: Vec<Vec<String>>,
found: Vec<Value>,
}
impl PropertyFinder<'_, '_, '_> {
fn in_scope(&mut self, declared: DeclaredNames, walk: impl FnOnce(&mut Self)) {
self.shadowed.push(declared.0);
walk(self);
self.shadowed.pop();
}
}
#[derive(Default)]
struct DeclaredNames(Vec<String>);
impl<'a> Visit<'a> for DeclaredNames {
fn visit_binding_identifier(&mut self, it: &BindingIdentifier<'a>) {
self.0.push(it.name.as_str().to_owned());
}
}
fn declared(visit: impl FnOnce(&mut DeclaredNames)) -> DeclaredNames {
let mut names = DeclaredNames::default();
visit(&mut names);
names
}
impl<'a> Visit<'a> for PropertyFinder<'_, '_, 'a> {
fn visit_function(&mut self, it: &Function<'a>, flags: oxc_semantic::ScopeFlags) {
let names = declared(|names| names.visit_function(it, flags));
self.in_scope(names, |finder| walk::walk_function(finder, it, flags));
}
fn visit_arrow_function_expression(&mut self, it: &ArrowFunctionExpression<'a>) {
let names = declared(|names| names.visit_arrow_function_expression(it));
self.in_scope(names, |finder| walk::walk_arrow_function_expression(finder, it));
}
fn visit_block_statement(&mut self, it: &BlockStatement<'a>) {
let names = declared(|names| names.visit_block_statement(it));
self.in_scope(names, |finder| walk::walk_block_statement(finder, it));
}
fn visit_for_statement(&mut self, it: &ForStatement<'a>) {
let names = declared(|names| names.visit_for_statement(it));
self.in_scope(names, |finder| walk::walk_for_statement(finder, it));
}
fn visit_for_in_statement(&mut self, it: &ForInStatement<'a>) {
let names = declared(|names| names.visit_for_in_statement(it));
self.in_scope(names, |finder| walk::walk_for_in_statement(finder, it));
}
fn visit_for_of_statement(&mut self, it: &ForOfStatement<'a>) {
let names = declared(|names| names.visit_for_of_statement(it));
self.in_scope(names, |finder| walk::walk_for_of_statement(finder, it));
}
fn visit_catch_clause(&mut self, it: &CatchClause<'a>) {
let names = declared(|names| names.visit_catch_clause(it));
self.in_scope(names, |finder| walk::walk_catch_clause(finder, it));
}
fn visit_object_property(&mut self, it: &ObjectProperty<'a>) {
let callable = self.call_with.and_then(|_| Callable::of_expression(&it.value));
let wanted = match self.call_with {
None => !it.method,
Some(_) => callable.is_some() && it.kind == oxc_ast::ast::PropertyKind::Init,
};
if wanted
&& it
.key
.static_name()
.is_some_and(|name| crate::js_string(name.as_js_str()) == self.key)
{
let eval = Evaluator::in_file(self.top, self.bindings, self.file, std::mem::take(&mut self.evaluated));
let shadowed: Frame<'_, '_> = self
.shadowed
.iter()
.flatten()
.map(|name| (name.clone(), Binding::Value(Value::Unknown)))
.collect();
if !shadowed.is_empty() {
eval.frames.borrow_mut().push(shadowed);
}
let value = match (self.call_with, callable) {
(Some(argument), Some(callable)) => eval.call_with(callable, Value::from(argument.clone()), 0),
_ => eval.expr(&it.value, 0),
};
self.found.push(value);
self.evaluated = eval.evaluated.into_inner();
}
walk::walk_object_property(self, it);
}
}
#[derive(Clone, Copy)]
enum Exported<'p, 'a> {
Expression(&'p Expression<'a>),
Function(&'p Function<'a>),
}
fn exported<'p, 'a>(program: &'p Program<'a>) -> Option<Exported<'p, 'a>> {
for stmt in &program.body {
match stmt {
Statement::ExportDefaultDeclaration(e) => {
return match &e.declaration {
ExportDefaultDeclarationKind::FunctionDeclaration(f) => Some(Exported::Function(f)),
ExportDefaultDeclarationKind::ClassDeclaration(_)
| ExportDefaultDeclarationKind::TSInterfaceDeclaration(_) => None,
other => other.as_expression().map(Exported::Expression),
};
}
Statement::TSExportAssignment(e) => return Some(Exported::Expression(&e.expression)),
Statement::ExpressionStatement(s) if is_module_exports_assignment(stmt) => {
if let Expression::AssignmentExpression(a) = &s.expression {
return Some(Exported::Expression(&a.right));
}
}
_ => {}
}
}
None
}
fn exports_changed(program: &Program<'_>) -> bool {
let mut statements = program
.body
.iter()
.skip_while(|stmt| !is_module_exports_assignment(stmt));
let unstable = Unstable::default();
statements.next().is_some()
&& statements.any(|stmt| {
let writes = Writes::of_statement(stmt, &unstable);
writes
.assigned
.iter()
.chain(writes.changed.iter().map(|(name, _)| name))
.any(|name| name == "module" || name == "exports")
})
}
fn is_module_exports_assignment(stmt: &Statement<'_>) -> bool {
if let Statement::ExpressionStatement(s) = stmt
&& let Expression::AssignmentExpression(a) = &s.expression
&& let AssignmentTarget::StaticMemberExpression(m) = &a.left
{
matches!(&m.object, Expression::Identifier(id) if id.name.as_str() == "module")
&& m.property.name.as_str() == "exports"
} else {
false
}
}
fn node_count(value: &Value) -> usize {
1 + match value {
Value::Array(items) | Value::OneOf(items) => items.iter().map(node_count).sum(),
Value::Object(entries) => entries.iter().map(|(_, item)| node_count(item)).sum(),
Value::Maybe(value) => node_count(value),
_ => 0,
}
}
struct Evaluator<'c, 'p, 'a> {
consts: &'c HashMap<String, &'p Expression<'a>>,
changed: &'c HashMap<String, TopChange>,
unstable: &'c Unstable,
functions: &'c HashMap<String, Callable<'p, 'a>>,
bindings: &'c ModuleBindings,
file: &'c str,
dir: &'c str,
path: &'c Path,
imports: &'c HashMap<String, ImportBinding>,
modules: Option<&'c Modules<'c>>,
evaluated: RefCell<HashMap<String, Value>>,
chain: RefCell<Vec<String>>,
frames: RefCell<Vec<Frame<'p, 'a>>>,
calls: Cell<usize>,
config_setter: RefCell<Option<String>>,
captured: RefCell<Option<Value>>,
arguments_of: RefCell<HashMap<Span, Vec<Value>>>,
}
static NO_IMPORTS: LazyLock<HashMap<String, ImportBinding>> = LazyLock::new(HashMap::default);
impl<'c, 'p, 'a> Evaluator<'c, 'p, 'a> {
fn new(
top: &'c TopLevel<'p, 'a>,
bindings: &'c ModuleBindings,
imports: &'c HashMap<String, ImportBinding>,
file: &'c str,
dir: &'c str,
path: &'c Path,
modules: Option<&'c Modules<'c>>,
) -> Self {
Self {
consts: &top.consts,
changed: &top.changed,
unstable: &top.unstable,
functions: &top.functions,
bindings,
file,
dir,
path,
imports,
modules,
evaluated: RefCell::new(HashMap::default()),
chain: RefCell::new(Vec::new()),
frames: RefCell::new(Vec::new()),
calls: Cell::new(0),
config_setter: RefCell::new(None),
captured: RefCell::new(None),
arguments_of: RefCell::new(HashMap::default()),
}
}
fn in_file(
top: &'c TopLevel<'p, 'a>,
bindings: &'c ModuleBindings,
file: &'c str,
evaluated: HashMap<String, Value>,
) -> Self {
let eval = Self::new(top, bindings, &NO_IMPORTS, file, ".", Path::new(file), None);
*eval.evaluated.borrow_mut() = evaluated;
eval
}
}
impl<'p, 'a> Evaluator<'_, 'p, 'a> {
fn exported_value(&self, exported: Exported<'p, 'a>, changed_later: bool) -> Value {
let callable = match exported {
Exported::Expression(expr) => Callable::of_expression(expr),
Exported::Function(function) => Some(Callable::Function(function)),
};
*self.config_setter.borrow_mut() = callable.and_then(first_parameter);
let value = match exported {
Exported::Expression(expr) => self.expr(expr, 0),
Exported::Function(function) => self.invoke(Callable::Function(function), None, false, 0),
};
let value = if changed_later { changed(value) } else { value };
self.captured.borrow_mut().take().unwrap_or(value)
}
fn exported_call_arguments(&self, exported: Exported<'p, 'a>, value: &Value) -> Vec<Value> {
match exported {
Exported::Expression(expr) => self.unknown_call_arguments(expr, value, 0),
Exported::Function(function) => self.returned_call_arguments(Callable::Function(function), value, 0),
}
}
fn expr(&self, expr: &'p Expression<'a>, depth: u32) -> Value {
if depth > MAX_DEPTH {
return Value::Unknown;
}
let depth = depth + 1;
let inner = expr.get_inner_expression();
match inner {
Expression::NullLiteral(_) => Value::Null,
Expression::BooleanLiteral(b) => Value::Bool(b.value),
Expression::NumericLiteral(n) => Value::Number(n.value),
Expression::StringLiteral(_) => static_string(inner).map_or(Value::Unknown, Value::String),
Expression::RegExpLiteral(r) => Value::Regex {
pattern: r.regex.pattern.text.as_str().to_owned(),
flags: r.regex.flags.to_string(),
},
Expression::TemplateLiteral(template) => self.template(template, depth),
Expression::BinaryExpression(b) => self.binary(b, depth),
Expression::StaticMemberExpression(member) => self.member(member, depth),
Expression::ComputedMemberExpression(member) => self.computed_member(member, depth),
Expression::ChainExpression(chain) => self.chain(chain, depth),
Expression::NewExpression(new) => self.url(new, depth),
Expression::UnaryExpression(u) if u.operator == UnaryOperator::UnaryNegation => {
match self.expr(&u.argument, depth) {
Value::Number(n) => Value::Number(-n),
_ => Value::Unknown,
}
}
Expression::UnaryExpression(u) if u.operator == UnaryOperator::LogicalNot => self
.truthy(&u.argument, depth)
.map_or(Value::Unknown, |t| Value::Bool(!t)),
Expression::LogicalExpression(l) => self.logical(l, depth),
Expression::ConditionalExpression(c) => self.conditional(c, depth),
Expression::AwaitExpression(a) => self.expr(&a.argument, depth),
Expression::ArrayExpression(a) => self.array(&a.elements, depth),
Expression::ObjectExpression(o) => self.object(&o.properties, depth),
Expression::Identifier(id) => self.reference(id, depth),
Expression::CallExpression(c) => self.call(c, depth),
Expression::ArrowFunctionExpression(f) => self.invoke(Callable::Arrow(f), None, false, depth),
Expression::FunctionExpression(f) => self.invoke(Callable::Function(f), None, false, depth),
_ => Value::Unknown,
}
}
fn reference(&self, id: &IdentifierReference<'a>, depth: u32) -> Value {
let value = self.identifier(id.name.as_str(), depth);
let Some(change) = self.unstable.of_reference(id) else {
return value;
};
let (value, definite) = value.split();
Value::doubtful(unstable(value, change), definite)
}
fn identifier(&self, name: &str, depth: u32) -> Value {
if let Some(binding) = self.lookup(name) {
return match binding {
Binding::Value(value) | Binding::Alias(Alias { value, .. }) => value,
Binding::Callable(_) => Value::Unknown,
};
}
let Some(&init) = self.consts.get(name) else {
return match self.functions.get(name) {
Some(callable) => self.invoke(*callable, None, true, depth),
None => match self.import_value(name, None) {
Some(value) => {
let (value, definite) = value.split();
Value::doubtful(self.changed_top(name, value), definite)
}
None => self.global(name),
},
};
};
if let Some(known) = self.evaluated.borrow().get(name) {
return known.clone();
}
{
let chain = self.chain.borrow();
if chain.len() >= MAX_CONST_CHAIN || chain.iter().any(|active| active == name) {
return Value::Unknown;
}
}
self.chain.borrow_mut().push(name.to_owned());
let saved = std::mem::take(&mut *self.frames.borrow_mut());
let cuts = self.modules.map(|modules| modules.cuts.get());
let (value, definite) = self.expr(init, 0).split();
*self.frames.borrow_mut() = saved;
self.chain.borrow_mut().pop();
let value = if node_count(&value) > MAX_CONST_NODES {
Value::Unknown
} else {
self.changed_top(name, value)
};
let value = Value::doubtful(value, definite);
if self.modules.map(|modules| modules.cuts.get()) == cuts {
self.evaluated.borrow_mut().insert(name.to_owned(), value.clone());
}
value
}
fn changed_top(&self, name: &str, value: Value) -> Value {
match self.changed.get(name) {
Some(TopChange::Assigned) => changed(value),
written => {
let mut value = value;
if let Some(TopChange::Written(writes)) = written {
for write in writes {
edit_at(&mut value, &write.keys, &write.edit());
}
}
match self.unstable.of_top(name) {
Some(change) => unstable(value, change),
None => value,
}
}
}
}
fn lookup(&self, name: &str) -> Option<Binding<'p, 'a>> {
self.frames
.borrow()
.iter()
.rev()
.find_map(|frame| frame.get(name).cloned())
}
fn is_local(&self, name: &str) -> bool {
self.frames.borrow().iter().any(|frame| frame.contains_key(name))
}
fn assign(&self, name: &str, binding: Binding<'p, 'a>) {
let mut frames = self.frames.borrow_mut();
if let Some(held) = frames.iter_mut().rev().find_map(|frame| frame.get_mut(name)) {
*held = binding;
} else if let Some(frame) = frames.last_mut() {
frame.insert(name.to_owned(), binding);
}
}
fn global(&self, name: &str) -> Value {
match name {
"__dirname" => Value::String(normalized(&[self.dir])),
"__filename" => Value::String(self.file_path()),
"undefined" => Value::Undefined,
_ => Value::Unknown,
}
}
fn file_path(&self) -> String {
normalized(&[self.dir, self.file])
}
fn import_value(&self, name: &str, args: Option<&[Value]>) -> Option<Value> {
let binding = self.imports.get(name)?;
self.modules?
.evaluate(self.path, &binding.specifier, &binding.imported, args)
}
fn spread_key(&self, expr: &Expression<'_>) -> &'static str {
if self.computes_itself(expr, 0) {
OWN_SPREAD
} else {
UNKNOWN_SPREAD
}
}
fn computes_itself(&self, expr: &Expression<'_>, depth: usize) -> bool {
if depth > MAX_CONST_CHAIN {
return false;
}
let depth = depth + 1;
match expr.get_inner_expression() {
Expression::AwaitExpression(e) => self.computes_itself(&e.argument, depth),
Expression::StaticMemberExpression(member) => self.computes_itself(&member.object, depth),
Expression::ComputedMemberExpression(member) => self.computes_itself(&member.object, depth),
Expression::ChainExpression(chain) => {
chain_object(chain).is_some_and(|object| self.computes_itself(object, depth))
}
Expression::CallExpression(call) => self.computes_itself(&call.callee, depth),
Expression::ConditionalExpression(conditional) => {
self.computes_itself(&conditional.consequent, depth)
|| self.computes_itself(&conditional.alternate, depth)
}
Expression::LogicalExpression(logical) => {
self.computes_itself(&logical.left, depth) || self.computes_itself(&logical.right, depth)
}
Expression::Identifier(id) => {
let name = id.name.as_str();
self.is_local(name)
|| self.functions.contains_key(name)
|| self
.consts
.get(name)
.is_some_and(|init| !self.takes_imported(init, depth))
}
_ => false,
}
}
fn takes_imported(&self, expr: &Expression<'_>, depth: usize) -> bool {
if depth > MAX_CONST_CHAIN {
return false;
}
let depth = depth + 1;
match expr.get_inner_expression() {
Expression::AwaitExpression(e) => self.takes_imported(&e.argument, depth),
Expression::StaticMemberExpression(member) => self.takes_imported(&member.object, depth),
Expression::ComputedMemberExpression(member) => self.takes_imported(&member.object, depth),
Expression::ChainExpression(chain) => {
chain_object(chain).is_some_and(|object| self.takes_imported(object, depth))
}
Expression::CallExpression(call) => {
let required = matches!(
call.callee.get_inner_expression(),
Expression::Identifier(id) if id.name.as_str() == "require"
);
required || self.takes_imported(&call.callee, depth)
}
Expression::Identifier(id) => {
let name = id.name.as_str();
self.imports.contains_key(name)
|| (!self.functions.contains_key(name)
&& self
.consts
.get(name)
.is_some_and(|init| self.takes_imported(init, depth)))
}
_ => false,
}
}
fn imported_call(&self, name: &str, call: &'p CallExpression<'a>, depth: u32) -> Option<Value> {
let binding = self.imports.get(name)?;
let modules = self.modules?;
if !modules.follows(self.path, &binding.specifier) {
return None;
}
let args = self.arguments(call, depth);
modules.evaluate(self.path, &binding.specifier, &binding.imported, Some(&args))
}
fn namespace_member(&self, namespace: &str, name: &str) -> Value {
let (Some(binding), Some(modules)) = (self.imports.get(namespace), self.modules) else {
return Value::Unknown;
};
modules
.evaluate(self.path, &binding.specifier, &Imported::named(name.to_owned()), None)
.unwrap_or(Value::Unknown)
}
fn call_named(&self, name: &str, args: &[Value]) -> Value {
match self.functions.get(name) {
Some(callable) => self.invoke(*callable, Some(args), true, 0),
None => self.import_value(name, Some(args)).unwrap_or(Value::Unknown),
}
}
fn required_value(&self, call: &'p CallExpression<'a>, depth: u32) -> Option<Value> {
let modules = self.modules?;
if !matches!(call.callee.get_inner_expression(), Expression::Identifier(id) if id.name.as_str() == "require") {
return None;
}
let specifier = single_argument(call).and_then(static_string)?;
if depth >= 2 && !specifier.starts_with('.') {
return None;
}
modules.evaluate(self.path, &specifier, &Imported::Default, None)
}
fn binary(&self, binary: &'p BinaryExpression<'a>, depth: u32) -> Value {
let equal = match binary.operator {
BinaryOperator::Addition => None,
BinaryOperator::StrictEquality => Some(true),
BinaryOperator::StrictInequality => Some(false),
_ => return Value::Unknown,
};
let (left, right) = (self.expr(&binary.left, depth), self.expr(&binary.right, depth));
match equal {
None => each_combination(&[left, right], |pair| match pair {
[left, right] => add(left, right),
_ => Value::Unknown,
}),
Some(equal) => strictly_equal(&left, &right).map_or(Value::Unknown, |same| Value::Bool(same == equal)),
}
}
fn template(&self, template: &'p TemplateLiteral<'a>, depth: u32) -> Value {
let mut substitutions = Vec::with_capacity(template.expressions.len());
for expression in &template.expressions {
let value = self.expr(expression, depth);
if !matches!(value, Value::OneOf(_)) && primitive_text(&value).is_none() {
return Value::Unknown;
}
substitutions.push(value);
}
each_combination(&substitutions, |texts| {
let mut out = String::new();
for (index, quasi) in template.quasis.iter().enumerate() {
out.push_str(
&quasi
.value
.cooked
.map_or_else(|| quasi.value.raw.as_str().to_owned(), crate::js_string),
);
if let Some(value) = texts.get(index) {
let Some(text) = primitive_text(value) else {
return Value::Unknown;
};
out.push_str(&text);
}
}
Value::String(out)
})
}
fn read(&self, expr: &'p Expression<'a>, depth: u32) -> (Value, bool) {
self.expr(expr, depth).split()
}
fn truthy(&self, expr: &'p Expression<'a>, depth: u32) -> Option<bool> {
match self.read(expr, depth) {
(value, true) => truthiness(&value),
(_, false) => None,
}
}
fn member(&self, member: &'p StaticMemberExpression<'a>, depth: u32) -> Value {
self.member_of(&member.object, member.property.name.as_str(), depth)
}
fn computed_member(&self, member: &'p ComputedMemberExpression<'a>, depth: u32) -> Value {
self.member_key(&member.expression, depth)
.map_or(Value::Unknown, |key| self.member_of(&member.object, &key, depth))
}
fn member_of(&self, object: &'p Expression<'a>, property: &str, depth: u32) -> Value {
self.special_member(object, property, depth)
.unwrap_or_else(|| property_of(&self.expr(object, depth), property))
}
fn special_member(&self, object: &'p Expression<'a>, property: &str, depth: u32) -> Option<Value> {
Some(match (object.get_inner_expression(), property) {
(Expression::ImportMeta(_), "dirname" | "dir") => Value::String(normalized(&[self.dir])),
(Expression::ImportMeta(_), "filename" | "url" | "path") => Value::String(self.file_path()),
(Expression::ImportMeta(_), "file") => Value::String(self.file.to_owned()),
(url @ Expression::NewExpression(_), "pathname") => self.expr(url, depth),
(Expression::Identifier(namespace), _) if self.is_namespace(namespace.name.as_str()) => {
let name = namespace.name.as_str();
let exports = Value::Object(vec![(property.to_owned(), self.namespace_member(name, property))]);
let exports = self.changed_top(name, exports);
let exports = match self.unstable.of_reference(namespace) {
Some(change) => unstable(exports, change),
None => exports,
};
property_of(&exports, property)
}
_ => return None,
})
}
fn member_key(&self, key: &'p Expression<'a>, depth: u32) -> Option<String> {
match self.expr(key, depth) {
Value::String(key) => Some(key),
Value::Number(number) => number_text(number),
_ => None,
}
}
fn chain(&self, chain: &'p ChainExpression<'a>, depth: u32) -> Value {
let link = match &chain.expression {
ChainElement::StaticMemberExpression(m) => {
self.chain_member(&m.object, Some(m.property.name.as_str().to_owned()), m.optional, depth)
}
ChainElement::ComputedMemberExpression(m) => {
let key = self.member_key(&m.expression, depth);
self.chain_member(&m.object, key, m.optional, depth)
}
ChainElement::CallExpression(call) => Some(self.call(call, depth)),
ChainElement::TSNonNullExpression(e) => self.chain_link(&e.expression, depth),
ChainElement::PrivateFieldExpression(_) => Some(Value::Unknown),
};
link.unwrap_or(Value::Undefined)
}
fn chain_link(&self, expr: &'p Expression<'a>, depth: u32) -> Option<Value> {
match expr.get_inner_expression() {
Expression::StaticMemberExpression(m) => {
self.chain_member(&m.object, Some(m.property.name.as_str().to_owned()), m.optional, depth)
}
Expression::ComputedMemberExpression(m) => {
let key = self.member_key(&m.expression, depth);
self.chain_member(&m.object, key, m.optional, depth)
}
_ => Some(self.expr(expr, depth)),
}
}
fn chain_member(
&self,
object: &'p Expression<'a>,
key: Option<String>,
optional: bool,
depth: u32,
) -> Option<Value> {
if let Some(special) = key.as_deref().and_then(|key| self.special_member(object, key, depth)) {
return Some(special);
}
let base = self.chain_link(object, depth)?;
if optional && matches!(base, Value::Null | Value::Undefined) {
return None;
}
Some(key.map_or(Value::Unknown, |key| property_of(&base, &key)))
}
fn is_namespace(&self, name: &str) -> bool {
self.imports
.get(name)
.is_some_and(|binding| binding.imported == Imported::Namespace)
&& !self.is_local(name)
}
fn logical(&self, logical: &'p LogicalExpression<'a>, depth: u32) -> Value {
let (left, definite) = self.read(&logical.left, depth);
if !definite {
return match logical.operator {
LogicalOperator::And => Value::Unknown,
LogicalOperator::Or | LogicalOperator::Coalesce => {
let right = self.read(&logical.right, depth).0;
Value::doubtful(any_of(&[Some(left), Some(right)]), false)
}
};
}
match left_is_value(logical.operator, &left) {
Some(true) => left,
Some(false) => self.expr(&logical.right, depth),
None => Value::Unknown,
}
}
fn possibilities(&self, expr: &'p Expression<'a>, depth: u32) -> Vec<Value> {
if depth > MAX_DEPTH {
return vec![Value::Unknown];
}
match expr.get_inner_expression() {
Expression::LogicalExpression(logical) => {
let (left, definite) = self.read(&logical.left, depth);
let primitive = is_primitive(&logical.left);
let decided = match logical.operator {
LogicalOperator::Coalesce if primitive && !is_void(&logical.left) => Some(true),
_ if !definite => None,
operator => left_is_value(operator, &left),
};
let left = if primitive || logical.operator == LogicalOperator::And {
Value::Null
} else {
left
};
match decided {
Some(true) => alternatives(left),
Some(false) => self.possibilities(&logical.right, depth + 1),
None => {
let mut out = alternatives(left);
out.extend(self.possibilities(&logical.right, depth + 1));
out
}
}
}
Expression::ConditionalExpression(conditional) => match self.truthy(&conditional.test, depth) {
Some(true) => self.possibilities(&conditional.consequent, depth + 1),
Some(false) => self.possibilities(&conditional.alternate, depth + 1),
None => {
let mut out = self.possibilities(&conditional.consequent, depth + 1);
out.extend(self.possibilities(&conditional.alternate, depth + 1));
out
}
},
_ => match self.expr(expr, depth) {
Value::Maybe(value) => {
let mut out = vec![Value::Undefined];
out.extend(alternatives(*value));
out
}
value => alternatives(value),
},
}
}
fn spread(&self, argument: &'p Expression<'a>, out: &mut Vec<(String, Value)>, depth: u32) {
let branches: Option<Vec<Vec<(String, Value)>>> = self
.possibilities(argument, depth)
.into_iter()
.map(spread_entries)
.collect();
match branches {
None => out.push((self.spread_key(argument).to_owned(), Value::Unknown)),
Some(mut branches) if branches.len() == 1 => out.extend(branches.pop().unwrap_or_default()),
Some(branches) => {
let added = spread_any(out, &branches);
out.extend(added);
}
}
}
fn conditional(&self, conditional: &'p ConditionalExpression<'a>, depth: u32) -> Value {
match self.truthy(&conditional.test, depth) {
Some(true) => self.expr(&conditional.consequent, depth),
Some(false) => self.expr(&conditional.alternate, depth),
None => either(
&self.expr(&conditional.consequent, depth),
&self.expr(&conditional.alternate, depth),
),
}
}
fn url(&self, new: &'p NewExpression<'a>, depth: u32) -> Value {
let is_url =
matches!(new.callee.get_inner_expression(), Expression::Identifier(id) if id.name.as_str() == "URL");
let mut args = new
.arguments
.iter()
.map(|arg| arg.as_expression().map_or(Value::Unknown, |e| self.expr(e, depth)));
match (is_url, args.next(), args.next(), args.next()) {
(true, Some(Value::String(relative)), Some(Value::String(base)), None)
if !relative.starts_with('/') && !relative.contains(':') && !base.contains(':') =>
{
Value::String(normalized(&[base.as_str(), "..", relative.as_str()]))
}
_ => Value::Unknown,
}
}
fn is_path_function(&self, callee: &Expression<'_>, wanted: &str) -> bool {
match callee.get_inner_expression() {
Expression::Identifier(id) => self
.bindings
.path_functions
.get(id.name.as_str())
.is_some_and(|function| function == wanted),
Expression::StaticMemberExpression(member) => {
member.property.name.as_str() == wanted
&& matches!(
member.object.get_inner_expression(),
Expression::Identifier(object) if self.bindings.path_modules.contains(object.name.as_str())
)
}
_ => false,
}
}
fn path_call(&self, call: &'p CallExpression<'a>, depth: u32) -> Option<Value> {
let function = match call.callee.get_inner_expression() {
Expression::Identifier(id) => {
let name = id.name.as_str();
if self.bindings.file_url_to_path.contains(name) {
"fileURLToPath"
} else {
self.bindings.path_functions.get(name)?.as_str()
}
}
Expression::StaticMemberExpression(member) => {
let object = match member.object.get_inner_expression() {
Expression::StaticMemberExpression(inner) if inner.property.name.as_str() == "posix" => {
inner.object.get_inner_expression()
}
other => other,
};
let Expression::Identifier(object) = object else {
return None;
};
let (object, property) = (object.name.as_str(), member.property.name.as_str());
let path_function = self.bindings.path_modules.contains(object) && PATH_FUNCTIONS.contains(&property);
let url_function = self.bindings.url_modules.contains(object) && property == "fileURLToPath";
if path_function || url_function {
property
} else if object == "process" && property == "cwd" {
return Some(Value::String(".".to_owned()));
} else {
return None;
}
}
_ => return None,
};
Some(each_combination(&self.arguments(call, depth), |args| {
apply_path_function(function, args)
}))
}
fn arguments(&self, call: &'p CallExpression<'a>, depth: u32) -> Vec<Value> {
let values: Vec<Value> = call
.arguments
.iter()
.map(|arg| arg.as_expression().map_or(Value::Unknown, |e| self.expr(e, depth)))
.collect();
self.arguments_of
.borrow_mut()
.entry(call.span)
.or_insert_with(|| values.clone());
values
}
fn package_directory(&self, call: &'p CallExpression<'a>, depth: u32) -> Option<Value> {
if !self.is_path_function(&call.callee, "dirname") {
return None;
}
let mut resolved = single_argument(call)?.get_inner_expression();
if let Expression::CallExpression(to_path) = resolved
&& self.is_file_url_to_path(&to_path.callee)
{
resolved = single_argument(to_path)?.get_inner_expression();
}
let Expression::CallExpression(resolve) = resolved else {
return None;
};
if !is_module_resolve(resolve) {
return None;
}
let manifest = single_argument(resolve)?.get_inner_expression();
if let Expression::CallExpression(join) = manifest
&& self.is_path_function(&join.callee, "join")
{
let arguments: Vec<&Expression<'a>> = join
.arguments
.iter()
.map(Argument::as_expression)
.collect::<Option<_>>()?;
let (file, segments) = arguments.split_last()?;
if segments.is_empty() || static_string(file).as_deref() != Some("package.json") {
return None;
}
let segments: Option<Vec<String>> = segments
.iter()
.copied()
.map(|segment| match self.expr(segment, depth) {
Value::String(text) => Some(text),
_ => None,
})
.collect();
return match segments {
Some(segments) => manifest_directory(&segments.join("/")),
None => Some(Value::Unknown),
};
}
let Value::String(manifest) = self.expr(manifest, depth) else {
return None;
};
manifest_directory(manifest.strip_suffix("/package.json")?)
}
fn is_file_url_to_path(&self, callee: &Expression<'_>) -> bool {
match callee.get_inner_expression() {
Expression::Identifier(id) => self.bindings.file_url_to_path.contains(id.name.as_str()),
Expression::StaticMemberExpression(member) => {
member.property.name.as_str() == "fileURLToPath"
&& matches!(
member.object.get_inner_expression(),
Expression::Identifier(object) if self.bindings.url_modules.contains(object.name.as_str())
)
}
_ => false,
}
}
fn resolve_call(&self, call: &'p CallExpression<'a>, depth: u32) -> Option<Value> {
let known = is_module_resolve(call) || is_resolve_of(call, "Promise");
known.then(|| single_argument(call).map_or(Value::Unknown, |arg| self.expr(arg, depth)))
}
fn glob_call(&self, call: &'p CallExpression<'a>, depth: u32) -> Option<Value> {
let is_glob = match call.callee.get_inner_expression() {
Expression::Identifier(id) => self.bindings.glob_names.contains(id.name.as_str()),
Expression::StaticMemberExpression(member) => {
GLOB_MEMBERS.contains(&member.property.name.as_str())
&& matches!(
member.object.get_inner_expression(),
Expression::Identifier(object) if self.bindings.glob_names.contains(object.name.as_str())
)
}
_ => false,
};
if !is_glob {
return None;
}
Some(match call.arguments.first().and_then(Argument::as_expression) {
Some(patterns) => match self.read(patterns, depth).0 {
Value::String(pattern) => Value::Array(vec![Value::String(pattern)]),
array @ Value::Array(_) => array,
_ => Value::Unknown,
},
None => Value::Unknown,
})
}
fn flat_config_call(&self, call: &'p CallExpression<'a>, depth: u32) -> Option<Value> {
let combines = match call.callee.get_inner_expression() {
Expression::Identifier(id) => self.bindings.flat_config_functions.contains(id.name.as_str()),
Expression::StaticMemberExpression(member) => {
member.property.name.as_str() == "config"
&& matches!(
member.object.get_inner_expression(),
Expression::Identifier(object) if self.bindings.flat_config_modules.contains(object.name.as_str())
)
}
_ => false,
};
if !combines {
return None;
}
let mut list = Vec::new();
for argument in &call.arguments {
let value = match argument {
Argument::SpreadElement(spread) => self.expr(&spread.argument, depth),
other => other.as_expression().map_or(Value::Unknown, |e| self.expr(e, depth)),
};
flatten_into(value, &mut list);
}
Some(Value::Array(list))
}
fn local_call(&self, call: &'p CallExpression<'a>, depth: u32) -> Option<Value> {
let Expression::Identifier(id) = call.callee.get_inner_expression() else {
return None;
};
let name = id.name.as_str();
let (callable, isolate) = match self.local_function(name) {
Some(LocalBinding::Function(callable)) => (callable, false),
Some(LocalBinding::Other) => return None,
None => match self.functions.get(name) {
Some(callable) => (*callable, true),
None => return self.imported_call(name, call, depth),
},
};
Some(self.invoke(callable, Some(self.arguments(call, depth).as_slice()), isolate, depth))
}
fn local_function(&self, name: &str) -> Option<LocalBinding<'p, 'a>> {
self.frames.borrow().iter().rev().find_map(|frame| {
frame.get(name).map(|binding| match binding {
Binding::Callable(callable) => LocalBinding::Function(*callable),
Binding::Value(_) | Binding::Alias(_) => LocalBinding::Other,
})
})
}
fn callback(&self, expr: &'p Expression<'a>) -> Option<(Callable<'p, 'a>, bool)> {
if let Some(callable) = Callable::of_expression(expr) {
return Some((callable, false));
}
let Expression::Identifier(id) = expr.get_inner_expression() else {
return None;
};
let name = id.name.as_str();
match self.local_function(name) {
Some(LocalBinding::Function(callable)) => Some((callable, false)),
Some(LocalBinding::Other) => None,
None => self.functions.get(name).map(|callable| (*callable, true)),
}
}
fn array_callback_call(&self, call: &'p CallExpression<'a>, depth: u32) -> Option<Value> {
let Expression::StaticMemberExpression(member) = call.callee.get_inner_expression() else {
return None;
};
let method = member.property.name.as_str();
if !ARRAY_CALLBACK_METHODS.contains(&method) {
return None;
}
let (callable, isolate) = self.callback(call.arguments.first()?.as_expression()?)?;
let (Value::Array(items), mut definite) = self.read(&member.object, depth) else {
return None;
};
let mut out = Vec::new();
let mut indexed = true;
for (index, item) in items.into_iter().enumerate() {
if item == Value::Unknown {
indexed = false;
out.push(Value::Unknown);
continue;
}
let index = u32::try_from(index)
.ok()
.filter(|_| indexed)
.map_or(Value::Unknown, |index| Value::Number(f64::from(index)));
let args = [item.clone(), index, Value::Unknown];
let (result, result_definite) = self.invoke(callable, Some(args.as_slice()), isolate, depth).split();
match method {
"map" => out.push(Value::doubtful(result, result_definite)),
"flatMap" => match result {
Value::Array(listed) => {
out.extend(listed);
definite &= result_definite;
}
Value::OneOf(_) => out.push(Value::Unknown),
other => out.push(Value::doubtful(other, result_definite)),
},
_ => match truthiness(&result).filter(|_| result_definite) {
Some(false) => {}
Some(true) => out.push(item),
None => {
definite = false;
out.push(item);
}
},
}
}
Some(Value::doubtful(Value::Array(out), definite))
}
fn object_from_entries(&self, call: &'p CallExpression<'a>, depth: u32) -> Option<Value> {
let Expression::StaticMemberExpression(member) = call.callee.get_inner_expression() else {
return None;
};
let on_object = matches!(
member.object.get_inner_expression(),
Expression::Identifier(object) if object.name.as_str() == "Object"
);
if !on_object || member.property.name.as_str() != "fromEntries" || self.binds("Object") {
return None;
}
let Some(argument) = single_argument(call) else {
return Some(Value::Unknown);
};
let (Value::Array(pairs), definite) = self.read(argument, depth) else {
return Some(Value::Unknown);
};
let mut entries: Vec<(String, Value)> = Vec::new();
for pair in pairs {
let Some((key, value)) = entry_pair(pair) else {
let marker = self.spread_key(argument);
if entries.last().is_none_or(|(last, _)| last != marker) {
entries.push((marker.to_owned(), Value::Unknown));
}
continue;
};
let value = Value::doubtful(value, definite);
let found = entries
.iter()
.rposition(|(name, _)| *name == key || ConfigValue::is_spread_key(name))
.filter(|&at| entries[at].0 == key);
match found {
Some(at) => entries[at].1 = value,
None => entries.push((key, value)),
}
}
Some(Value::Object(entries))
}
fn merge_call(&self, call: &'p CallExpression<'a>, depth: u32) -> Option<Value> {
let style = self.merge_style(call)?;
if takes_options(&call.callee, call.arguments.len()) {
self.arguments(call, depth);
return Some(Value::Unknown);
}
let mut merged: Option<Vec<(String, Value)>> = None;
let expressions = call.arguments.iter().map(Argument::as_expression);
let mut values: Vec<_> = self.arguments(call, depth).into_iter().zip(expressions).collect();
if style == MergeStyle::FirstWins {
values.reverse();
}
for (value, expression) in values {
let (value, definite) = value.split();
match value {
Value::Null | Value::Undefined | Value::Bool(_) => {}
Value::Object(entries) => {
let entries = entries
.into_iter()
.map(|(key, value)| (key, Value::doubtful(value, definite)))
.collect();
let base = merged.get_or_insert_with(Vec::new);
merge_objects(base, entries, style != MergeStyle::Shallow);
}
_ => {
let key = expression.map_or(UNKNOWN_SPREAD, |expression| self.spread_key(expression));
merge_objects(
merged.get_or_insert_with(Vec::new),
vec![(key.to_owned(), Value::Unknown)],
false,
);
}
}
}
Some(merged.map_or(Value::Unknown, Value::Object))
}
fn filter_call(&self, call: &'p CallExpression<'a>, depth: u32) -> Option<Value> {
let Expression::StaticMemberExpression(member) = call.callee.get_inner_expression() else {
return None;
};
let by_boolean = matches!(
single_argument(call).map(Expression::get_inner_expression),
Some(Expression::Identifier(id))
if id.name.as_str() == "Boolean" && !self.binds("Boolean")
);
if member.property.name.as_str() != "filter" || !by_boolean {
return None;
}
let (items, mut definite) = match member.object.get_inner_expression() {
Expression::ArrayExpression(array) => self.filtered_elements(&array.elements, depth),
object => match self.read(object, depth) {
(Value::Array(items), definite) => (items, definite),
_ => return None,
},
};
let mut kept = Vec::new();
for item in items {
if let Some((value, may_drop)) = truthy_part(item) {
definite &= !may_drop;
kept.push(value);
}
}
Some(Value::doubtful(Value::Array(kept), definite))
}
fn binds(&self, name: &str) -> bool {
self.is_local(name)
|| self.consts.contains_key(name)
|| self.functions.contains_key(name)
|| self.imports.contains_key(name)
}
fn filtered_elements(&self, elements: &'p [ArrayExpressionElement<'a>], depth: u32) -> (Vec<Value>, bool) {
let mut items = Vec::new();
let mut definite = true;
for element in elements {
if let Some(Expression::LogicalExpression(and)) =
element.as_expression().map(Expression::get_inner_expression)
&& and.operator == LogicalOperator::And
&& self.truthy(&and.left, depth).is_none()
{
items.push(self.expr(&and.right, depth));
definite = false;
continue;
}
let (part, part_definite) = self.array(std::slice::from_ref(element), depth).split();
if let Value::Array(part) = part {
items.extend(part);
}
definite &= part_definite;
}
(items, definite)
}
fn capture_set(&self, call: &'p CallExpression<'a>, depth: u32) -> bool {
let Some(setter) = self.config_setter.borrow().clone() else {
return false;
};
let Expression::StaticMemberExpression(member) = call.callee.get_inner_expression() else {
return false;
};
let on_setter =
matches!(member.object.get_inner_expression(), Expression::Identifier(id) if id.name.as_str() == setter);
let Some(argument) = single_argument(call).filter(|_| on_setter && member.property.name.as_str() == "set")
else {
return false;
};
let value = self.expr(argument, depth);
*self.captured.borrow_mut() = Some(value);
true
}
fn call(&self, call: &'p CallExpression<'a>, depth: u32) -> Value {
if self.capture_set(call, depth) {
return Value::Unknown;
}
let special = self
.required_value(call, depth)
.or_else(|| required_package(call, depth))
.or_else(|| self.package_directory(call, depth))
.or_else(|| self.path_call(call, depth))
.or_else(|| self.resolve_call(call, depth))
.or_else(|| self.glob_call(call, depth))
.or_else(|| self.flat_config_call(call, depth))
.or_else(|| self.local_call(call, depth))
.or_else(|| self.merge_call(call, depth))
.or_else(|| self.filter_call(call, depth))
.or_else(|| self.array_callback_call(call, depth))
.or_else(|| self.object_from_entries(call, depth));
if let Some(value) = special {
return value;
}
self.defined(call)
.map_or(Value::Unknown, |first| self.expr(first, depth))
}
fn defined(&self, call: &'p CallExpression<'a>) -> Option<&'p Expression<'a>> {
defined(call).filter(|_| self.unstable.calls_foreign(call))
}
fn merge_style(&self, call: &CallExpression<'_>) -> Option<MergeStyle> {
merge_style(&call.callee, self.bindings).filter(|_| self.unstable.calls_foreign(call))
}
fn unknown_call_arguments(&self, expr: &'p Expression<'a>, value: &Value, depth: u32) -> Vec<Value> {
if depth > MAX_DEPTH {
return Vec::new();
}
let depth = depth + 1;
match expr.get_inner_expression() {
Expression::AwaitExpression(a) => self.unknown_call_arguments(&a.argument, value, depth),
Expression::CallExpression(call) => self.unknown_arguments_of_call(call, value, depth),
Expression::Identifier(id) => self.constant_call_arguments(id.name.as_str(), value, depth),
Expression::ArrowFunctionExpression(f) => self.returned_call_arguments(Callable::Arrow(f), value, depth),
Expression::FunctionExpression(f) => self.returned_call_arguments(Callable::Function(f), value, depth),
Expression::ObjectExpression(object) => self.object_call_arguments(&object.properties, value, depth),
Expression::ArrayExpression(array) => self.array_call_arguments(&array.elements, value, depth),
_ => Vec::new(),
}
}
fn nested_call_arguments(&self, expr: &'p Expression<'a>, value: &Value, depth: u32) -> Vec<Value> {
if depth > MAX_DEPTH {
return Vec::new();
}
let depth = depth + 1;
match expr.get_inner_expression() {
Expression::AwaitExpression(a) => self.nested_call_arguments(&a.argument, value, depth),
Expression::CallExpression(call) => self.unknown_arguments_of_call(call, value, depth),
Expression::ObjectExpression(object) => self.object_call_arguments(&object.properties, value, depth),
Expression::ArrayExpression(array) => self.array_call_arguments(&array.elements, value, depth),
_ => Vec::new(),
}
}
fn object_call_arguments(&self, properties: &'p [ObjectPropertyKind<'a>], value: &Value, depth: u32) -> Vec<Value> {
let object = value.definite();
let spread = if object.get(UNKNOWN_SPREAD).is_some() || object.get(OWN_SPREAD).is_some() {
Value::Unknown
} else {
Value::Null
};
let mut out = Vec::new();
for property in properties {
match property {
ObjectPropertyKind::ObjectProperty(p) => {
let Some(key) = p.key.static_name() else {
continue;
};
let key = crate::js_string(key.as_js_str());
let field = object.get(&key).cloned().unwrap_or(Value::Unknown);
out.extend(
self.nested_call_arguments(&p.value, &field, depth)
.into_iter()
.map(|argument| Value::Object(vec![(key.clone(), argument)])),
);
}
ObjectPropertyKind::SpreadProperty(s) => {
out.extend(self.nested_call_arguments(&s.argument, &spread, depth));
}
}
}
out
}
fn array_call_arguments(
&self,
elements: &'p [ArrayExpressionElement<'a>],
value: &Value,
depth: u32,
) -> Vec<Value> {
let unknown = Value::Unknown;
let items: &[Value] = match value.definite() {
Value::Array(items) => items,
_ => &[],
};
elements
.iter()
.take_while(|element| !matches!(element, ArrayExpressionElement::SpreadElement(_)))
.enumerate()
.filter_map(|(index, element)| Some((element.as_expression()?, items.get(index).unwrap_or(&unknown))))
.flat_map(|(element, item)| self.nested_call_arguments(element, item, depth))
.collect()
}
fn unknown_arguments_of_call(&self, call: &'p CallExpression<'a>, value: &Value, depth: u32) -> Vec<Value> {
if let Some(first) = self.defined(call) {
return self.unknown_call_arguments(first, value, depth);
}
let recorded = self.arguments_of.borrow().get(&call.span).cloned();
if self.merge_style(call).is_some() {
let values = recorded.unwrap_or_default();
return call
.arguments
.iter()
.zip(&values)
.filter_map(|(argument, value)| Some((argument.as_expression()?, value)))
.flat_map(|(argument, value)| self.unknown_call_arguments(argument, value, depth))
.collect();
}
if !matches!(value.definite(), Value::Unknown | Value::OneOf(_)) {
return Vec::new();
}
recorded.unwrap_or_else(|| self.arguments(call, depth))
}
fn constant_call_arguments(&self, name: &str, value: &Value, depth: u32) -> Vec<Value> {
if self.is_local(name) || self.functions.contains_key(name) {
return Vec::new();
}
let Some(&init) = self.consts.get(name) else {
return Vec::new();
};
let saved = std::mem::take(&mut *self.frames.borrow_mut());
let arguments = self.unknown_call_arguments(init, value, depth);
*self.frames.borrow_mut() = saved;
arguments
}
fn returned_call_arguments(&self, callable: Callable<'p, 'a>, value: &Value, depth: u32) -> Vec<Value> {
let (params, body, statements) = match callable {
Callable::Arrow(f) => (&f.params, f.get_expression(), f.get_function_body()),
Callable::Function(f) => (&f.params, None, f.body.as_deref()),
};
let returned = body.or_else(|| match statements?.statements.as_slice() {
[only] => single_return(only)?.argument.as_ref(),
_ => None,
});
let Some(returned) = returned else {
return Vec::new();
};
self.frames.borrow_mut().push(Frame::default());
for param in ¶ms.items {
self.bind_pattern(¶m.pattern, Value::Unknown, depth);
}
if let Some(param) = ¶ms.rest {
self.bind_pattern(¶m.rest.argument, Value::Unknown, depth);
}
let arguments = self.unknown_call_arguments(returned, value, depth);
self.frames.borrow_mut().pop();
arguments
}
fn invoke(&self, callable: Callable<'p, 'a>, args: Option<&[Value]>, isolate: bool, depth: u32) -> Value {
if self.calls.get() >= MAX_CALLS {
return Value::Unknown;
}
self.calls.set(self.calls.get() + 1);
let params = match callable {
Callable::Function(f) => &f.params,
Callable::Arrow(f) => &f.params,
};
let saved = if isolate {
std::mem::take(&mut *self.frames.borrow_mut())
} else {
Vec::new()
};
self.frames.borrow_mut().push(Frame::default());
for (index, param) in params.items.iter().enumerate() {
let value = match (args.map(|args| args.get(index)), ¶m.initializer) {
(Some(Some(value)), Some(default)) => self.defaulted(value.clone(), default, depth),
(Some(Some(value)), None) => value.clone(),
(Some(None), Some(default)) => self.expr(default, depth),
(None | Some(None), _) => Value::Unknown,
};
self.bind_pattern(¶m.pattern, value, depth);
}
if let Some(param) = ¶ms.rest {
self.bind_pattern(¶m.rest.argument, Value::Unknown, depth);
}
let value = match callable {
Callable::Arrow(f) => match f.get_expression() {
Some(body) => self.expr(body, depth),
None => f
.get_function_body()
.map_or(Value::Unknown, |b| self.returned(&b.statements, depth)),
},
Callable::Function(f) => f
.body
.as_ref()
.map_or(Value::Unknown, |b| self.returned(&b.statements, depth)),
};
self.frames.borrow_mut().pop();
if isolate {
*self.frames.borrow_mut() = saved;
}
value
}
fn call_with(&self, callable: Callable<'p, 'a>, argument: Value, depth: u32) -> Value {
let statements = match callable {
Callable::Arrow(f) => f.get_function_body().map(|body| &body.statements),
Callable::Function(f) => f.body.as_ref().map(|body| &body.statements),
};
let (Some(name), Some(statements)) = (first_parameter(callable), statements) else {
return self.invoke(callable, Some(std::slice::from_ref(&argument)), false, depth);
};
let params = match callable {
Callable::Function(f) => &f.params,
Callable::Arrow(f) => &f.params,
};
self.frames.borrow_mut().push(Frame::default());
self.bind(&name, argument);
for param in params.items.iter().skip(1) {
self.bind_pattern(¶m.pattern, Value::Unknown, depth);
}
if let Some(param) = ¶ms.rest {
self.bind_pattern(¶m.rest.argument, Value::Unknown, depth);
}
let returned = self.run(statements, depth);
let frame = self.frames.borrow_mut().pop();
returned.unwrap_or_else(|| match frame.and_then(|mut frame| frame.remove(&name)) {
Some(Binding::Value(value)) => value,
_ => Value::Unknown,
})
}
fn bind_pattern(&self, pattern: &'p BindingPattern<'a>, value: Value, depth: u32) {
match pattern {
BindingPattern::BindingIdentifier(id) => self.bind(id.name.as_str(), value),
BindingPattern::AssignmentPattern(p) => {
let value = self.defaulted(value, &p.right, depth);
self.bind_pattern(&p.left, value, depth);
}
BindingPattern::ObjectPattern(object) => {
for property in &object.properties {
let field = property.key.static_name().map_or(Value::Unknown, |key| {
property_of(&value, &crate::js_string(key.as_js_str()))
});
self.bind_pattern(&property.value, field, depth);
}
if let Some(rest) = &object.rest {
self.bind_pattern(&rest.argument, Value::Unknown, depth);
}
}
BindingPattern::ArrayPattern(_) => {
for id in pattern.get_binding_identifiers() {
self.bind(id.name.as_str(), Value::Unknown);
}
}
}
}
fn defaulted(&self, value: Value, default: &'p Expression<'a>, depth: u32) -> Value {
match value {
Value::Undefined => self.expr(default, depth),
Value::Maybe(value) => {
let default = self.read(default, depth).0;
Value::doubtful(any_of(&[Some(*value), Some(default)]), false)
}
value => value,
}
}
fn bind(&self, name: &str, value: Value) {
self.bind_to(name, Binding::Value(value));
}
fn bind_to(&self, name: &str, binding: Binding<'p, 'a>) {
if let Some(frame) = self.frames.borrow_mut().last_mut() {
frame.insert(name.to_owned(), binding);
}
}
fn returned(&self, statements: &'p [Statement<'a>], depth: u32) -> Value {
if let [Statement::ExpressionStatement(s)] = statements {
return self.expr(&s.expression, depth);
}
self.run(statements, depth).unwrap_or(Value::Unknown)
}
fn run(&self, statements: &'p [Statement<'a>], depth: u32) -> Option<Value> {
for (index, statement) in statements.iter().enumerate() {
match statement {
Statement::ReturnStatement(r) => {
if let Some(argument) = &r.argument {
self.havoc(Writes::of_expression(argument, self.unstable));
}
return Some(r.argument.as_ref().map_or(Value::Unknown, |e| self.expr(e, depth)));
}
Statement::ExpressionStatement(s) => self.mutation(&s.expression, depth),
Statement::BlockStatement(b) => {
if let Some(value) = self.block(&b.body, depth) {
return Some(value);
}
}
Statement::IfStatement(i) => {
self.havoc(Writes::of_expression(&i.test, self.unstable));
let taken = match self.truthy(&i.test, depth) {
Some(true) => Some(&i.consequent),
Some(false) => i.alternate.as_ref(),
None => {
if !returns(statement) {
self.either_branch(i, depth);
continue;
}
let rest = &statements[index + 1..];
return Some(self.either_return(i, rest, depth));
}
};
if let Some(value) = taken.and_then(|branch| self.block(std::slice::from_ref(branch), depth)) {
return Some(value);
}
}
other if returns(other) => return Some(Value::Unknown),
other => match other.as_declaration() {
Some(Declaration::VariableDeclaration(v)) => {
self.havoc(Writes::of_statement(other, self.unstable));
self.declare(v, depth);
}
Some(Declaration::FunctionDeclaration(f)) => {
if let Some(id) = &f.id {
self.bind_to(id.name.as_str(), Binding::Callable(Callable::Function(f)));
}
}
Some(_) => {}
None => self.havoc(Writes::of_statement(other, self.unstable)),
},
}
}
None
}
fn block(&self, statements: &'p [Statement<'a>], depth: u32) -> Option<Value> {
self.frames.borrow_mut().push(Frame::default());
let value = self.run(statements, depth);
self.frames.borrow_mut().pop();
let mut vars = Vars::default();
for statement in statements {
vars.visit_statement(statement);
}
for name in vars.0 {
self.assign(&name, Binding::Value(Value::Unknown));
}
value
}
fn either_branch(&self, statement: &'p IfStatement<'a>, depth: u32) {
let before = self.frames.borrow().clone();
let captured = self.captured.borrow().clone();
self.block(std::slice::from_ref(&statement.consequent), depth);
let consequent = std::mem::replace(&mut *self.frames.borrow_mut(), before);
let consequent_captured = std::mem::replace(&mut *self.captured.borrow_mut(), captured);
if let Some(alternate) = &statement.alternate {
self.block(std::slice::from_ref(alternate), depth);
}
let mut frames = self.frames.borrow_mut();
for (frame, other) in frames.iter_mut().zip(consequent) {
let mut names: Vec<String> = frame.keys().chain(other.keys()).cloned().collect();
names.sort_unstable();
names.dedup();
for name in names {
let joined = match (frame.get(&name), other.get(&name)) {
(Some(Binding::Value(a)), Some(Binding::Value(b))) => Binding::Value(join(a, b)),
(Some(Binding::Alias(a)), Some(Binding::Alias(b))) if a.same_part(b) => Binding::Alias(Alias {
value: join(&a.value, &b.value),
..a.clone()
}),
(Some(Binding::Callable(a)), Some(Binding::Callable(b))) if a.same(*b) => Binding::Callable(*a),
_ => Binding::Value(Value::Unknown),
};
frame.insert(name, joined);
}
}
let mut captured = self.captured.borrow_mut();
if *captured != consequent_captured {
*captured = Some(match (&*captured, &consequent_captured) {
(Some(a), Some(b)) => either(a, b),
_ => Value::Unknown,
});
}
}
fn either_return(&self, statement: &'p IfStatement<'a>, rest: &'p [Statement<'a>], depth: u32) -> Value {
let path = |branch: Option<&'p Statement<'a>>| -> Option<Value> {
match branch.map(single_return) {
Some(Some(r)) => Some(r.argument.as_ref().map_or(Value::Unknown, |e| self.expr(e, depth))),
Some(None) if branch.is_some_and(returns) => Some(Value::Unknown),
_ => None,
}
};
let consequent = path(Some(&statement.consequent));
let alternate = path(statement.alternate.as_ref());
let after = || {
for branch in [Some(&statement.consequent), statement.alternate.as_ref()]
.into_iter()
.flatten()
{
if !returns(branch) {
self.havoc(Writes::of_statement(branch, self.unstable));
}
}
self.run(rest, depth).unwrap_or(Value::Unknown)
};
let (a, b) = match (consequent, alternate) {
(Some(a), Some(b)) => (a, b),
(Some(a), None) => (a, after()),
(None, Some(b)) => (after(), b),
(None, None) => return Value::Unknown,
};
either(&a, &b)
}
fn declare(&self, declaration: &'p VariableDeclaration<'a>, depth: u32) {
for declarator in &declaration.declarations {
if let BindingPattern::BindingIdentifier(id) = &declarator.id
&& let Some(callable) = declarator.init.as_ref().and_then(Callable::of_expression)
{
self.bind_to(id.name.as_str(), Binding::Callable(callable));
continue;
}
let value = declarator
.init
.as_ref()
.map_or(Value::Unknown, |init| self.expr(init, depth));
let value = if node_count(&value) > MAX_CONST_NODES {
Value::Unknown
} else {
value
};
let aliases = self.aliases(declaration.kind, declarator);
self.bind_pattern(&declarator.id, value, depth);
for (name, alias) in aliases {
self.bind_alias(&name, alias);
}
}
}
fn aliases(&self, kind: VariableDeclarationKind, declarator: &'p VariableDeclarator<'a>) -> Vec<(String, Alias)> {
if kind != VariableDeclarationKind::Const {
return Vec::new();
}
let Some((root, keys)) = declarator.init.as_ref().and_then(|init| alias_source(init)) else {
return Vec::new();
};
let root = root.name.as_str();
let Some(frame) = self.frames.borrow().iter().rposition(|frame| frame.contains_key(root)) else {
return Vec::new();
};
alias_names(&declarator.id)
.unwrap_or_default()
.into_iter()
.filter(|(id, _)| self.unstable.follows(id))
.map(|(id, key)| {
let mut keys = keys.clone();
keys.extend(key);
let alias = Alias {
value: Value::Unknown,
root: root.to_owned(),
frame,
keys,
};
(id.name.as_str().to_owned(), alias)
})
.collect()
}
fn bind_alias(&self, name: &str, mut alias: Alias) {
let mut frames = self.frames.borrow_mut();
if let Some(binding) = frames.last_mut().and_then(|frame| frame.get_mut(name))
&& let Binding::Value(value) = &mut *binding
{
alias.value = std::mem::replace(value, Value::Unknown);
*binding = Binding::Alias(alias);
}
}
fn havoc(&self, writes: Writes<'_>) {
for name in writes.assigned {
self.assign(&name, Binding::Value(Value::Unknown));
}
let mut changed = writes.changed;
changed.sort_unstable();
changed.dedup();
for (name, write) in changed {
self.edit(&name, &write.keys, &write.edit());
}
}
fn mutation(&self, expr: &'p Expression<'a>, depth: u32) {
match expr.get_inner_expression() {
Expression::AwaitExpression(a) => self.mutation(&a.argument, depth),
Expression::AssignmentExpression(a) => {
self.havoc(Writes::of_expression(&a.right, self.unstable));
let assign = a.operator == AssignmentOperator::Assign;
match &a.left {
AssignmentTarget::AssignmentTargetIdentifier(id) => {
let value = if assign {
self.expr(&a.right, depth)
} else {
Value::Unknown
};
self.assign(id.name.as_str(), Binding::Value(value));
}
AssignmentTarget::StaticMemberExpression(m) => match reach(&m.object) {
Some((root, keys, true)) => {
let value = if assign {
self.expr(&a.right, depth)
} else {
Value::Unknown
};
let edit = Edit::Set(m.property.name.as_str().to_owned(), value);
self.edit(root.name.as_str(), &keys, &edit);
}
Some((root, keys, false)) => self.edit(root.name.as_str(), &keys, &Edit::Taint),
None => {
let mut writes = Writes::new(self.unstable);
writes.visit_assignment_target(&a.left);
self.havoc(writes.through_aliases());
}
},
_ => self.havoc(Writes::of_expression(expr, self.unstable)),
}
}
Expression::CallExpression(c) => {
if !self.capture_set(c, depth) && !self.insert(c, depth) {
self.havoc(Writes::of_effect(expr, self.unstable));
}
}
_ => self.havoc(Writes::of_expression(expr, self.unstable)),
}
}
fn insert(&self, call: &'p CallExpression<'a>, depth: u32) -> bool {
let Expression::StaticMemberExpression(member) = call.callee.get_inner_expression() else {
return false;
};
let front = match member.property.name.as_str() {
"push" => false,
"unshift" => true,
_ => return false,
};
let Some((root, keys, true)) = reach(&member.object) else {
return false;
};
let Some(arguments) = call
.arguments
.iter()
.map(Argument::as_expression)
.collect::<Option<Vec<_>>>()
else {
return false;
};
if !self.is_list(root.name.as_str(), &keys) {
return false;
}
for argument in &arguments {
self.havoc(Writes::of_expression(argument, self.unstable));
}
let items = arguments
.into_iter()
.map(|argument| self.expr(argument, depth))
.collect();
self.edit(root.name.as_str(), &keys, &Edit::Insert { front, items });
true
}
fn is_list(&self, name: &str, keys: &[String]) -> bool {
let frames = self.frames.borrow();
let Some(binding) = frames.iter().rev().find_map(|frame| frame.get(name)) else {
return false;
};
match binding {
Binding::Value(value) | Binding::Alias(Alias { value, .. }) => {
value_at(value.definite(), keys).is_some_and(holds_list)
}
Binding::Callable(_) => false,
}
}
fn edit(&self, name: &str, keys: &[String], edit: &Edit) {
let mut frames = self.frames.borrow_mut();
if let Some(index) = frames.iter().rposition(|frame| frame.contains_key(name)) {
edit_binding(&mut frames, index, name, keys, edit);
}
}
fn array(&self, elements: &'p [ArrayExpressionElement<'a>], depth: u32) -> Value {
let mut out = Vec::new();
let mut definite = true;
for element in elements {
match element {
ArrayExpressionElement::SpreadElement(s) => {
let (items, spread_definite) = self.spread_elements(&s.argument, depth);
out.extend(items);
definite &= spread_definite;
}
other => out.push(other.as_expression().map_or(Value::Unknown, |e| self.expr(e, depth))),
}
}
Value::doubtful(Value::Array(out), definite)
}
fn spread_elements(&self, argument: &'p Expression<'a>, depth: u32) -> (Vec<Value>, bool) {
let unknown = || (vec![Value::Unknown], true);
let mut branches = self.possibilities(argument, depth);
if branches.len() == 1 {
return match branches.pop() {
Some(Value::Array(items)) => (items, true),
_ => unknown(),
};
}
let mut items: Vec<Value> = Vec::new();
for branch in branches {
match branch {
Value::Array(listed) => {
for item in listed {
if !items.contains(&item) {
items.push(item);
}
}
}
Value::Null | Value::Undefined | Value::Bool(_) => {}
_ => return unknown(),
}
}
(items, false)
}
fn object(&self, properties: &'p [ObjectPropertyKind<'a>], depth: u32) -> Value {
let mut out: Vec<(String, Value)> = Vec::new();
for property in properties {
match property {
ObjectPropertyKind::SpreadProperty(s) => self.spread(&s.argument, &mut out, depth),
ObjectPropertyKind::ObjectProperty(p) => {
let Some(key) = p.key.static_name() else {
out.push((UNKNOWN_SPREAD.to_owned(), Value::Unknown));
continue;
};
let value = if p.method || p.kind != oxc_ast::ast::PropertyKind::Init {
Value::Unknown
} else {
self.expr(&p.value, depth)
};
out.push((crate::js_string(key.as_js_str()), value));
}
}
}
Value::Object(out)
}
}
fn edit_binding(frames: &mut [Frame<'_, '_>], index: usize, name: &str, keys: &[String], edit: &Edit) {
let Some(binding) = frames.get_mut(index).and_then(|frame| frame.get_mut(name)) else {
return;
};
let (root, frame, part, before) = match binding {
Binding::Value(value) => return edit_at(value, keys, edit),
Binding::Callable(_) => return,
Binding::Alias(alias) => {
let before = alias.value.clone();
edit_at(&mut alias.value, keys, edit);
(alias.root.clone(), alias.frame, alias.keys.clone(), before)
}
};
let held = frames
.get(frame)
.and_then(|bindings| bindings.get(&root))
.and_then(|binding| match binding {
Binding::Value(value) | Binding::Alias(Alias { value, .. }) => value_at(value, &part),
Binding::Callable(_) => None,
});
if held == Some(&before) {
let mut path = part;
path.extend_from_slice(keys);
edit_binding(frames, frame, &root, &path, edit);
} else {
edit_binding(frames, frame, &root, &part, &Edit::Taint);
}
}
fn edit_at(value: &mut Value, keys: &[String], edit: &Edit) {
let target = value.definite_mut();
if let Value::OneOf(alternatives) = &mut *target {
for alternative in alternatives {
edit_at(alternative, keys, edit);
}
return;
}
let Some((key, rest)) = keys.split_first() else {
return match edit {
Edit::Set(key, field) => set_entry(target, key, field.clone()),
Edit::Insert { front, items } => insert_items(target, *front, items),
Edit::Taint => taint_value(target),
};
};
match target {
Value::Object(entries) => {
let found = entries
.iter()
.rposition(|(name, _)| name == key || ConfigValue::is_spread_key(name));
match found {
Some(at) if entries[at].0 == *key => edit_at(&mut entries[at].1, rest, edit),
Some(_) => open_entries(entries),
None if inherits(entries, key) => open_entries(entries),
None => {}
}
}
other => taint_value(other),
}
}
fn set_entry(value: &mut Value, key: &str, field: Value) {
match value.definite_mut() {
Value::Object(entries) => match entries.iter_mut().find(|(existing, _)| existing == key) {
Some((_, slot)) => *slot = field,
None => entries.push((key.to_owned(), field)),
},
Value::Array(items) => items.push(Value::Unknown),
_ => {}
}
}
fn holds_list(value: &Value) -> bool {
match value.definite() {
Value::Array(_) => true,
Value::OneOf(alternatives) => alternatives.iter().all(holds_list),
_ => false,
}
}
fn insert_items(value: &mut Value, front: bool, items: &[Value]) {
match value {
Value::Array(list) if front => {
list.splice(0..0, items.iter().cloned());
}
Value::Array(list) => list.extend(items.iter().cloned()),
other => taint_value(other),
}
}
fn taint_value(value: &mut Value) {
match value.definite_mut() {
Value::Object(entries) => open_entries(entries),
Value::Array(items) => {
items.iter_mut().for_each(open_below);
if !matches!(items.last(), Some(Value::Unknown)) {
items.push(Value::Unknown);
}
}
_ => {}
}
}
fn open_below(value: &mut Value) {
match value {
Value::Object(entries) => {
for (_, field) in entries.iter_mut() {
open_below(field);
}
open_entries(entries);
}
Value::Array(items) => {
items.iter_mut().for_each(open_below);
if !matches!(items.last(), Some(Value::Unknown)) {
items.push(Value::Unknown);
}
}
Value::OneOf(alternatives) => alternatives.iter_mut().for_each(open_below),
Value::Maybe(inner) => open_below(inner),
_ => {}
}
}
fn open_entries(entries: &mut Vec<(String, Value)>) {
if entries.last().is_none_or(|(key, _)| key != UNKNOWN_SPREAD) {
entries.push((UNKNOWN_SPREAD.to_owned(), Value::Unknown));
}
}
fn value_at<'v>(value: &'v Value, keys: &[String]) -> Option<&'v Value> {
let Some((key, rest)) = keys.split_first() else {
return Some(value);
};
let Value::Object(entries) = value else {
return None;
};
entries
.iter()
.rev()
.find(|(name, _)| name == key || ConfigValue::is_spread_key(name))
.filter(|(name, _)| name == key)
.and_then(|(_, child)| value_at(child, rest))
}
pub(crate) fn defined<'p, 'a>(call: &'p CallExpression<'a>) -> Option<&'p Expression<'a>> {
let callee = match call.callee.get_inner_expression() {
Expression::Identifier(id) => id.name.as_str(),
Expression::StaticMemberExpression(m) if !on_reflection(&m.object) => m.property.name.as_str(),
_ => return None,
};
let is_define = callee.starts_with("define") && callee.len() > "define".len();
call.arguments
.first()
.and_then(Argument::as_expression)
.filter(|_| is_define)
}
fn on_reflection(object: &Expression<'_>) -> bool {
matches!(
object.get_inner_expression(),
Expression::Identifier(id) if matches!(id.name.as_str(), "Object" | "Reflect")
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MergeStyle {
Deep,
Shallow,
FirstWins,
}
fn merge_style(callee: &Expression<'_>, bindings: &ModuleBindings) -> Option<MergeStyle> {
match callee.get_inner_expression() {
Expression::Identifier(id) => match id.name.as_str() {
name if bindings.foreign_merges.contains(name) => None,
"mergeConfig" | "merge" | "deepmerge" | "deepMerge" | "mergeDeep" | "mergeWith" => Some(MergeStyle::Deep),
"defu" => Some(MergeStyle::FirstWins),
_ => None,
},
Expression::StaticMemberExpression(member) => {
let object_is = |name: &str| matches!(member.object.get_inner_expression(), Expression::Identifier(o) if o.name.as_str() == name);
let merge_object = matches!(
member.object.get_inner_expression(),
Expression::Identifier(o)
if matches!(o.name.as_str(), "_" | "lodash") || bindings.merge_names.contains(o.name.as_str())
);
match member.property.name.as_str() {
"assign" if object_is("Object") => Some(MergeStyle::Shallow),
"merge" | "mergeWith" if merge_object => Some(MergeStyle::Deep),
"mergeConfig" | "deepmerge" | "mergeDeep" => Some(MergeStyle::Deep),
_ => None,
}
}
_ => None,
}
}
fn takes_options(callee: &Expression<'_>, count: usize) -> bool {
let name = match callee.get_inner_expression() {
Expression::Identifier(id) => id.name.as_str(),
Expression::StaticMemberExpression(member) => member.property.name.as_str(),
_ => return false,
};
match name {
"deepmerge" | "deepMerge" => count > 2,
"mergeWith" => true,
_ => false,
}
}
fn merge_objects(base: &mut Vec<(String, Value)>, incoming: Vec<(String, Value)>, deep: bool) {
for (key, value) in incoming {
let Some(index) = base.iter().position(|(k, _)| *k == key) else {
base.push((key, value));
continue;
};
if ConfigValue::is_spread_key(&key) {
continue;
}
let slot = &mut base[index].1;
*slot = merged_value(std::mem::replace(slot, Value::Unknown), value, deep);
}
}
fn merged_value(current: Value, incoming: Value, deep: bool) -> Value {
let (current, current_definite) = current.split();
let (incoming, incoming_definite) = incoming.split();
let merged = match (deep, current, incoming) {
(true, Value::Object(mut left), Value::Object(right)) => {
merge_objects(&mut left, right, true);
Value::Object(left)
}
(true, Value::Array(mut left), Value::Array(right)) => {
left.extend(right);
Value::Array(left)
}
(true, Value::Object(mut known), Value::Unknown | Value::OneOf(_))
| (true, Value::Unknown | Value::OneOf(_), Value::Object(mut known)) => {
if !known.iter().any(|(k, _)| ConfigValue::is_spread_key(k)) {
known.push((UNKNOWN_SPREAD.to_owned(), Value::Unknown));
}
Value::Object(known)
}
(true, Value::Array(mut known), Value::Unknown | Value::OneOf(_))
| (true, Value::Unknown | Value::OneOf(_), Value::Array(mut known)) => {
known.push(Value::Unknown);
Value::Array(known)
}
(_, current, incoming) if !incoming_definite => {
return Value::doubtful(any_of(&[Some(current), Some(incoming)]), false);
}
(_, _, incoming) => return incoming,
};
Value::doubtful(merged, current_definite && incoming_definite)
}
fn single_return<'p, 'a>(statement: &'p Statement<'a>) -> Option<&'p ReturnStatement<'a>> {
match statement {
Statement::ReturnStatement(r) => Some(r),
Statement::BlockStatement(b) => match b.body.as_slice() {
[only] => single_return(only),
_ => None,
},
_ => None,
}
}
fn returns(statement: &Statement<'_>) -> bool {
#[derive(Default)]
struct Finder(bool);
impl<'a> Visit<'a> for Finder {
fn visit_return_statement(&mut self, _: &ReturnStatement<'a>) {
self.0 = true;
}
fn visit_function(&mut self, _: &Function<'a>, _: oxc_semantic::ScopeFlags) {}
fn visit_arrow_function_expression(&mut self, _: &ArrowFunctionExpression<'a>) {}
fn visit_class(&mut self, _: &oxc_ast::ast::Class<'a>) {}
}
let mut finder = Finder::default();
finder.visit_statement(statement);
finder.0
}
struct Writes<'u> {
unstable: &'u Unstable,
assigned: Vec<String>,
changed: Vec<(String, Write)>,
aliases: Vec<(String, String)>,
}
impl Write {
fn edit(&self) -> Edit {
self.key
.clone()
.map_or(Edit::Taint, |key| Edit::Set(key, Value::Unknown))
}
}
impl<'u> Writes<'u> {
fn new(unstable: &'u Unstable) -> Self {
Self {
unstable,
assigned: Vec::new(),
changed: Vec::new(),
aliases: Vec::new(),
}
}
fn of_statement(statement: &Statement<'_>, unstable: &'u Unstable) -> Self {
let mut writes = Self::new(unstable);
writes.visit_statement(statement);
writes.through_aliases()
}
fn of_expression(expression: &Expression<'_>, unstable: &'u Unstable) -> Self {
let mut writes = Self::new(unstable);
writes.visit_expression(expression);
writes.through_aliases()
}
fn through_aliases(mut self) -> Self {
let mut index = 0;
while let Some((name, _)) = self.changed.get(index).cloned() {
for (alias, root) in &self.aliases {
let whole = |(changed, write): &(String, Write)| changed == root && *write == Write::default();
if *alias == name && !self.changed.iter().any(whole) {
self.changed.push((root.clone(), Write::default()));
}
}
index += 1;
}
self
}
fn of_effect(expression: &Expression<'_>, unstable: &'u Unstable) -> Self {
let mut writes = Self::new(unstable);
match expression.get_inner_expression() {
Expression::AssignmentExpression(a) => {
match a.left.as_simple_assignment_target() {
Some(target) => writes.visit_simple_assignment_target(target),
None => writes.visit_assignment_target(&a.left),
}
writes.visit_expression(&a.right);
}
Expression::CallExpression(c) => {
writes.visit_call_expression(c);
writes.arguments(c.span, &c.arguments, true);
}
Expression::AwaitExpression(a) => return Self::of_effect(&a.argument, unstable),
other => writes.visit_expression(other),
}
writes
}
fn object(&mut self, expr: &Expression<'_>) {
if let Some((root, keys, _)) = reach(expr) {
return self.change(root.name.as_str(), Write { keys, key: None });
}
let unstable = self.unstable;
for (name, keys, _) in unstable.parts_of(expr) {
self.change(
name,
Write {
keys: keys.clone(),
key: None,
},
);
}
}
fn change(&mut self, name: &str, write: Write) {
let change = (name.to_owned(), write);
if !self.changed.contains(&change) {
self.changed.push(change);
}
}
fn arguments(&mut self, span: Span, arguments: &[Argument<'_>], statement: bool) {
if !self.unstable.changes_arguments(span, statement) {
return;
}
for argument in arguments.iter().filter_map(given) {
for (root, keys) in given_parts(argument) {
if self.unstable.seen_after(root) {
self.change(root.name.as_str(), Write { keys, key: None });
}
}
}
}
}
impl<'a> Visit<'a> for Writes<'_> {
fn visit_variable_declarator(&mut self, it: &VariableDeclarator<'a>) {
if let Some((root, _)) = it.init.as_ref().and_then(|init| alias_source(init)) {
for (id, _) in alias_names(&it.id).unwrap_or_default() {
self.aliases
.push((id.name.as_str().to_owned(), root.name.as_str().to_owned()));
}
}
walk::walk_variable_declarator(self, it);
}
fn visit_simple_assignment_target(&mut self, it: &SimpleAssignmentTarget<'a>) {
match it {
SimpleAssignmentTarget::AssignmentTargetIdentifier(id) => self.assigned.push(id.name.as_str().to_owned()),
SimpleAssignmentTarget::StaticMemberExpression(m) => match reach(&m.object) {
Some((root, keys, true)) => {
let key = Some(m.property.name.as_str().to_owned());
self.change(root.name.as_str(), Write { keys, key });
}
Some(_) => self.object(&m.object),
None => {
let unstable = self.unstable;
for (name, keys, exact) in unstable.parts_of(&m.object) {
let key = exact.then(|| m.property.name.as_str().to_owned());
self.change(
name,
Write {
keys: keys.clone(),
key,
},
);
}
}
},
other => {
if let Some(member) = other.as_member_expression() {
self.object(member.object());
}
}
}
walk::walk_simple_assignment_target(self, it);
}
fn visit_assignment_target_property_identifier(&mut self, it: &AssignmentTargetPropertyIdentifier<'a>) {
self.assigned.push(it.binding.name.as_str().to_owned());
walk::walk_assignment_target_property_identifier(self, it);
}
fn visit_unary_expression(&mut self, it: &UnaryExpression<'a>) {
if it.operator == UnaryOperator::Delete
&& let Some(member) = it.argument.get_inner_expression().as_member_expression()
{
self.object(member.object());
}
walk::walk_unary_expression(self, it);
}
fn visit_call_expression(&mut self, it: &CallExpression<'a>) {
if let Expression::Identifier(callee) = it.callee.get_inner_expression()
&& self.unstable.calls_value(it)
{
self.change(callee.name.as_str(), Write::default());
}
if let Some(target) = changed_by_call(it, self.unstable.is_pure(it), self.unstable.calls_back(it))
&& !self.unstable.is_namespace(target)
{
match (property_defined(it), reach(target)) {
(Some(key), Some((root, keys, true))) => {
self.change(root.name.as_str(), Write { keys, key: Some(key) });
}
_ => self.object(target),
}
}
self.arguments(it.span, &it.arguments, false);
walk::walk_call_expression(self, it);
}
fn visit_new_expression(&mut self, it: &NewExpression<'a>) {
self.arguments(it.span, &it.arguments, false);
walk::walk_new_expression(self, it);
}
fn visit_expression_statement(&mut self, it: &ExpressionStatement<'a>) {
if let Some(call) = statement_call(&it.expression) {
self.arguments(call.span, &call.arguments, true);
}
walk::walk_expression_statement(self, it);
}
fn visit_with_statement(&mut self, it: &oxc_ast::ast::WithStatement<'a>) {
self.object(&it.object);
walk::walk_with_statement(self, it);
}
fn visit_function(&mut self, _: &Function<'a>, _: oxc_semantic::ScopeFlags) {}
fn visit_arrow_function_expression(&mut self, _: &ArrowFunctionExpression<'a>) {}
fn visit_class(&mut self, _: &oxc_ast::ast::Class<'a>) {}
}
#[derive(Default)]
struct Vars(Vec<String>);
impl<'a> Visit<'a> for Vars {
fn visit_variable_declaration(&mut self, it: &VariableDeclaration<'a>) {
if it.kind == VariableDeclarationKind::Var {
for declarator in &it.declarations {
self.0.extend(
declarator
.id
.get_binding_identifiers()
.iter()
.map(|id| id.name.as_str().to_owned()),
);
}
}
walk::walk_variable_declaration(self, it);
}
fn visit_function(&mut self, _: &Function<'a>, _: oxc_semantic::ScopeFlags) {}
fn visit_arrow_function_expression(&mut self, _: &ArrowFunctionExpression<'a>) {}
fn visit_class(&mut self, _: &oxc_ast::ast::Class<'a>) {}
}
fn changed(value: Value) -> Value {
match value {
Value::Object(mut entries) => {
open_entries(&mut entries);
Value::Object(entries)
}
Value::Array(mut items) => {
items.push(Value::Unknown);
Value::Array(items)
}
Value::Maybe(value) => Value::Maybe(Box::new(changed(*value))),
_ => Value::Unknown,
}
}
fn unstable(value: Value, change: &Change) -> Value {
let Change::Contents(writes) = change else {
return changed(value);
};
let mut value = value;
for write in writes {
edit_at(&mut value, &write.keys, &write.edit());
}
value
}
fn left_is_value(operator: LogicalOperator, left: &Value) -> Option<bool> {
match operator {
LogicalOperator::And => truthiness(left).map(|truthy| !truthy),
LogicalOperator::Or => truthiness(left),
LogicalOperator::Coalesce => match left {
Value::Unknown | Value::OneOf(_) | Value::Maybe(_) => None,
Value::Null | Value::Undefined => Some(false),
_ => Some(true),
},
}
}
fn is_primitive(expr: &Expression<'_>) -> bool {
match expr.get_inner_expression() {
Expression::BinaryExpression(binary) => {
binary.operator.is_equality() || binary.operator.is_compare() || binary.operator.is_relational()
}
Expression::UnaryExpression(unary) => matches!(unary.operator, UnaryOperator::LogicalNot | UnaryOperator::Void),
_ => false,
}
}
fn is_void(expr: &Expression<'_>) -> bool {
matches!(expr.get_inner_expression(), Expression::UnaryExpression(unary) if unary.operator == UnaryOperator::Void)
}
fn spread_entries(value: Value) -> Option<Vec<(String, Value)>> {
match value {
Value::Object(entries) => Some(entries),
Value::Null | Value::Undefined | Value::Bool(_) | Value::Number(_) => Some(Vec::new()),
Value::String(s) => s.is_empty().then(Vec::new),
Value::Array(_) | Value::Regex { .. } | Value::Unknown | Value::OneOf(_) | Value::Maybe(_) => None,
}
}
fn entry_value(entries: &[(String, Value)], key: &str) -> Option<Value> {
entries
.iter()
.rev()
.find(|(name, _)| name == key || ConfigValue::is_spread_key(name))
.map(|(name, value)| if name == key { value.clone() } else { Value::Unknown })
}
fn length_of(value: &Value) -> Value {
let length = match value {
Value::String(text) => Some(text.encode_utf16().count()),
Value::Array(items) if !items.contains(&Value::Unknown) => Some(items.len()),
_ => None,
};
length
.and_then(|length| u32::try_from(length).ok())
.map_or(Value::Unknown, |length| Value::Number(f64::from(length)))
}
fn property_of(value: &Value, key: &str) -> Value {
let definite = value.is_definite();
match (value.definite(), key) {
(_, "length") if !definite => Value::Unknown,
(known @ (Value::String(_) | Value::Array(_)), "length") => length_of(known),
(Value::Object(entries), _) => match entry_value(entries, key) {
Some(field) => Value::doubtful(field, definite),
None if inherits(entries, key) => Value::Unknown,
None => Value::Undefined,
},
(Value::Array(items), _) if definite => {
array_index(key).map_or(Value::Unknown, |index| element_at(items, index))
}
_ => Value::Unknown,
}
}
fn array_index(key: &str) -> Option<usize> {
let index: u32 = key.parse().ok()?;
(index.to_string() == key)
.then(|| usize::try_from(index).ok())
.flatten()
}
fn element_at(items: &[Value], index: usize) -> Value {
if items
.iter()
.take(index.saturating_add(1))
.any(|item| *item == Value::Unknown)
{
return Value::Unknown;
}
items.get(index).cloned().unwrap_or(Value::Undefined)
}
fn entry_pair(pair: Value) -> Option<(String, Value)> {
let Value::Array(parts) = pair else {
return None;
};
let mut parts = parts.into_iter();
let key = primitive_text(&parts.next()?)?;
Some((key, parts.next().unwrap_or(Value::Undefined)))
}
fn primitive_text(value: &Value) -> Option<String> {
match value {
Value::String(text) => Some(text.clone()),
Value::Number(number) => number_text(*number),
Value::Bool(b) => Some(b.to_string()),
_ => None,
}
}
fn number_text(number: f64) -> Option<String> {
if number == 0.0 {
return Some("0".to_owned());
}
(1e-6..1e21).contains(&number.abs()).then(|| format!("{number}"))
}
fn add(left: &Value, right: &Value) -> Value {
match (left, right) {
(Value::Number(a), Value::Number(b)) => Value::Number(a + b),
(Value::String(_), _) | (_, Value::String(_)) => match (primitive_text(left), primitive_text(right)) {
(Some(a), Some(b)) => Value::String(a + &b),
_ => Value::Unknown,
},
_ => Value::Unknown,
}
}
fn strictly_equal(left: &Value, right: &Value) -> Option<bool> {
let primitive = |value: &Value| {
matches!(
value,
Value::Null | Value::Undefined | Value::Bool(_) | Value::Number(_) | Value::String(_)
)
};
(primitive(left) && primitive(right)).then(|| left == right)
}
fn inherits(entries: &[(String, Value)], key: &str) -> bool {
OBJECT_PROTOTYPE_MEMBERS.contains(&key) || entries.iter().any(|(name, _)| name == "__proto__")
}
fn spread_any(before: &[(String, Value)], branches: &[Vec<(String, Value)>]) -> Vec<(String, Value)> {
let objects: Vec<Option<&[(String, Value)]>> = branches.iter().map(|branch| Some(branch.as_slice())).collect();
any_entries(&objects, before)
}
fn any_entries(objects: &[Option<&[(String, Value)]>], before: &[(String, Value)]) -> Vec<(String, Value)> {
let entries = || objects.iter().flatten().copied().flatten();
let mut keys: Vec<&str> = Vec::new();
for (key, _) in entries() {
if !ConfigValue::is_spread_key(key) && !keys.contains(&key.as_str()) {
keys.push(key);
}
}
let mut out: Vec<(String, Value)> = Vec::new();
for key in keys {
let fields: Vec<Option<(Value, bool)>> = objects
.iter()
.map(|object| {
object
.and_then(|object| entry_value(object, key))
.or_else(|| entry_value(before, key))
.map(Value::split)
})
.collect();
let each_definite = fields
.iter()
.all(|field| field.as_ref().is_some_and(|(_, definite)| *definite));
let values: Vec<Option<Value>> = fields.into_iter().map(|field| field.map(|(value, _)| value)).collect();
let definite = each_definite && agree(&values);
out.push((key.to_owned(), Value::doubtful(any_of(&values), definite)));
}
for marker in [UNKNOWN_SPREAD, OWN_SPREAD] {
if entries().any(|(key, _)| key == marker) {
out.push((marker.to_owned(), Value::Unknown));
}
}
out
}
fn any_of(values: &[Option<Value>]) -> Value {
let present: Vec<&Value> = values.iter().flatten().collect();
let Some(&first) = present.first() else {
return Value::Unknown;
};
if agree(values) {
return first.clone();
}
let lists: Option<Vec<&[Value]>> = present
.iter()
.map(|&value| match value {
Value::Array(list) => Some(list.as_slice()),
Value::String(_) => Some(std::slice::from_ref(value)),
_ => None,
})
.collect();
if let Some(lists) = lists
&& present.iter().any(|value| matches!(value, Value::Array(_)))
{
let mut items: Vec<Value> = Vec::new();
for item in lists.into_iter().flatten() {
if !items.contains(item) {
items.push(item.clone());
}
}
return Value::Array(items);
}
let objects: Option<Vec<Option<Entries<'_>>>> = values
.iter()
.map(|value| match value {
None => Some(None),
Some(Value::Object(entries)) => Some(Some(entries.as_slice())),
Some(_) => None,
})
.collect();
if let Some(objects) = objects {
return Value::Object(any_entries(&objects, &[]));
}
if present.len() < values.len() {
return Value::Unknown;
}
one_of(present.into_iter().cloned())
}
const MAX_ALTERNATIVES: usize = 8;
fn one_of(values: impl IntoIterator<Item = Value>) -> Value {
let mut out: Vec<Value> = Vec::new();
for value in values {
let nested = match value {
Value::Unknown | Value::Maybe(_) => return Value::Unknown,
Value::OneOf(nested) => nested,
other => vec![other],
};
for alternative in nested {
if !out.contains(&alternative) {
out.push(alternative);
}
}
if out.len() > MAX_ALTERNATIVES {
return Value::Unknown;
}
}
match out.len() {
0 => Value::Unknown,
1 => out.pop().unwrap_or(Value::Unknown),
_ => Value::OneOf(out),
}
}
fn each_combination(args: &[Value], apply: impl Fn(&[Value]) -> Value) -> Value {
if !args.iter().any(|arg| matches!(arg, Value::OneOf(_))) {
return apply(args);
}
let mut combinations: Vec<Vec<Value>> = vec![Vec::new()];
for arg in args {
let choices = alternatives(arg.clone());
if combinations.len().saturating_mul(choices.len()) > MAX_ALTERNATIVES {
return Value::Unknown;
}
let mut next = Vec::with_capacity(combinations.len() * choices.len());
for combination in &combinations {
for choice in &choices {
let mut extended = combination.clone();
extended.push(choice.clone());
next.push(extended);
}
}
combinations = next;
}
one_of(combinations.iter().map(|combination| apply(combination.as_slice())))
}
fn alternatives(value: Value) -> Vec<Value> {
match value {
Value::OneOf(alternatives) => alternatives,
other => vec![other],
}
}
fn truthy_part(value: Value) -> Option<(Value, bool)> {
match truthiness(&value) {
Some(false) => None,
Some(true) => Some((value, false)),
None => match value {
Value::OneOf(alternatives) => {
let count = alternatives.len();
let kept: Vec<Value> = alternatives
.into_iter()
.filter(|alternative| truthiness(alternative) != Some(false))
.collect();
let may_drop = kept.len() < count || kept.iter().any(|alternative| truthiness(alternative).is_none());
(!kept.is_empty()).then(|| (one_of(kept), may_drop))
}
other => Some((other, true)),
},
}
}
fn agree(values: &[Option<Value>]) -> bool {
values
.first()
.is_some_and(|first| first.is_some() && values.iter().all(|value| value == first))
}
fn truthiness(value: &Value) -> Option<bool> {
match value {
Value::Null | Value::Undefined => Some(false),
Value::Bool(b) => Some(*b),
Value::Number(n) => Some(*n != 0.0 && !n.is_nan()),
Value::String(s) => Some(!s.is_empty()),
Value::Array(_) | Value::Object(_) | Value::Regex { .. } => Some(true),
Value::Unknown | Value::OneOf(_) | Value::Maybe(_) => None,
}
}
fn either(a: &Value, b: &Value) -> Value {
if a == b {
return a.clone();
}
if !a.is_definite() || !b.is_definite() {
return Value::doubtful(either(a.definite(), b.definite()), false);
}
let (Value::Object(left), Value::Object(right)) = (a, b) else {
return one_of([a.clone(), b.clone()]);
};
let mut keys: Vec<&str> = Vec::new();
for (key, _) in left.iter().chain(right) {
if !ConfigValue::is_spread_key(key) && !keys.contains(&key.as_str()) {
keys.push(key);
}
}
let mut out: Vec<(String, Value)> = keys
.into_iter()
.map(|key| {
let value = match (a.get(key), b.get(key)) {
(Some(x), Some(y)) => either(x, y),
(Some(only), None) | (None, Some(only)) => Value::doubtful(Value::Unknown, only.is_definite()),
(None, None) => Value::Unknown,
};
(key.to_owned(), value)
})
.collect();
for marker in [UNKNOWN_SPREAD, OWN_SPREAD] {
if a.get(marker).is_some() || b.get(marker).is_some() {
out.push((marker.to_owned(), Value::Unknown));
}
}
Value::Object(out)
}
fn join(a: &Value, b: &Value) -> Value {
if !a.is_definite() || !b.is_definite() {
return Value::doubtful(join(a.definite(), b.definite()), false);
}
match (a, b) {
(Value::Array(x), Value::Array(y)) if x != y && (x.starts_with(y) || y.starts_with(x)) => {
let mut common = if x.len() < y.len() { x.clone() } else { y.clone() };
common.push(Value::Unknown);
Value::Array(common)
}
_ => either(a, b),
}
}
fn flatten_into(value: Value, list: &mut Vec<Value>) {
match value {
Value::Array(items) => {
for item in items {
flatten_into(item, list);
}
}
other => list.push(other),
}
}
fn single_argument<'p, 'a>(call: &'p CallExpression<'a>) -> Option<&'p Expression<'a>> {
let mut arguments = call.arguments.iter();
match (arguments.next(), arguments.next()) {
(Some(only), None) => only.as_expression(),
_ => None,
}
}
fn required_package(call: &CallExpression<'_>, depth: u32) -> Option<Value> {
if depth < 2 {
return None;
}
let call = match call.callee.get_inner_expression() {
Expression::CallExpression(factory) => &**factory,
_ => call,
};
let Expression::Identifier(callee) = call.callee.get_inner_expression() else {
return None;
};
if callee.name.as_str() != "require" {
return None;
}
let module = single_argument(call).and_then(static_string)?;
(!module.starts_with(['.', '/'])).then_some(Value::String(module))
}
fn manifest_directory(dir: &str) -> Option<Value> {
if dir.starts_with('.') {
return None;
}
let package = !dir.starts_with(['/', '#'])
&& !dir.contains([':', '\\'])
&& dir.split('/').all(|segment| !matches!(segment, "" | "." | ".."));
Some(if package {
Value::String(dir.to_owned())
} else {
Value::Unknown
})
}
fn is_module_resolve(call: &CallExpression<'_>) -> bool {
is_resolve_of(call, "require")
|| matches!(
call.callee.get_inner_expression(),
Expression::StaticMemberExpression(m)
if m.property.name.as_str() == "resolve"
&& matches!(m.object.get_inner_expression(), Expression::ImportMeta(_))
)
}
fn is_resolve_of(call: &CallExpression<'_>, object: &str) -> bool {
matches!(
call.callee.get_inner_expression(),
Expression::StaticMemberExpression(m)
if m.property.name.as_str() == "resolve"
&& matches!(m.object.get_inner_expression(), Expression::Identifier(o) if o.name.as_str() == object)
)
}
fn chain_object<'b, 'a>(chain: &'b ChainExpression<'a>) -> Option<&'b Expression<'a>> {
match &chain.expression {
ChainElement::CallExpression(call) => Some(&call.callee),
ChainElement::TSNonNullExpression(e) => Some(&e.expression),
element => element.as_member_expression().map(MemberExpression::object),
}
}