use oxc_ast::ast::{
Argument, AssignmentExpression, AssignmentTarget, BindingPattern, CallExpression, Expression,
MemberExpression, ObjectProperty, PropertyDefinition, PropertyKey, TaggedTemplateExpression,
VariableDeclarator,
};
use oxc_ast_visit::Visit;
use rustc_hash::FxHashSet;
use super::layout::Layout;
const DATABASE_WRITE_METHODS: [&str; 33] = [
"$executeRaw",
"$executeRawUnsafe",
"$transaction",
"batch",
"bulkCreate",
"bulkWrite",
"create",
"createMany",
"del",
"delete",
"deleteFrom",
"deleteMany",
"deleteOne",
"destroy",
"findByIdAndDelete",
"findByIdAndUpdate",
"findOneAndDelete",
"findOneAndReplace",
"findOneAndUpdate",
"insert",
"insertInto",
"insertMany",
"insertOne",
"remove",
"replaceOne",
"save",
"transaction",
"truncate",
"update",
"updateMany",
"updateOne",
"updateTable",
"upsert",
];
const STATEMENT_RUN_METHODS: [&str; 2] = ["exec", "run"];
const PREPARED_RUN_METHODS: [&str; 5] = ["all", "each", "get", "iterate", "run"];
const PREPARE_METHOD: &str = "prepare";
const SQL_READ_VERB: &str = "select";
const SQL_WRITE_VERBS: [&str; 10] = [
"alter", "create", "delete", "drop", "insert", "merge", "replace", "truncate", "update",
"upsert",
];
const RUNTIME_RECEIVERS: [&str; 2] = ["Object", "Reflect"];
pub(super) fn starts(source: &str, layout: Layout, direct: bool) -> Vec<u32> {
let (text, kind) = layout.parse_view(source);
let allocator = oxc_allocator::Allocator::default();
let parsed = oxc_parser::Parser::new(&allocator, &text, kind.source_type()).parse();
let mut writes = Writes {
direct,
..Writes::default()
};
writes.visit_program(&parsed.program);
writes.finish()
}
fn sql_verb(text: &str) -> &str {
let text = text.trim_start();
let verb_end = text
.find(|c: char| !c.is_ascii_alphabetic())
.unwrap_or(text.len());
&text[..verb_end]
}
fn is_sql_write(text: &str) -> bool {
let verb = sql_verb(text);
SQL_WRITE_VERBS
.iter()
.any(|write| write.eq_ignore_ascii_case(verb))
}
fn first_static_text<'b>(call: &'b CallExpression<'_>) -> Option<&'b str> {
match call.arguments.first() {
Some(Argument::StringLiteral(literal)) => Some(literal.value.as_str()),
Some(Argument::TemplateLiteral(template)) => template
.quasis
.first()
.map(|quasi| quasi.value.raw.as_str()),
Some(Argument::TaggedTemplateExpression(tagged)) => tagged
.quasi
.quasis
.first()
.map(|quasi| quasi.value.raw.as_str()),
_ => None,
}
}
fn is_route_path(call: &CallExpression<'_>) -> bool {
first_static_text(call).is_some_and(|text| text.starts_with('/'))
}
fn is_runtime_receiver(object: &Expression<'_>) -> bool {
matches!(object, Expression::Identifier(receiver)
if RUNTIME_RECEIVERS.contains(&receiver.name.as_str()))
}
fn is_regex(expression: &Expression<'_>) -> bool {
match expression.without_parentheses() {
Expression::RegExpLiteral(_) => true,
Expression::NewExpression(new) => {
matches!(&new.callee, Expression::Identifier(callee) if callee.name == "RegExp")
}
Expression::CallExpression(call) => {
matches!(&call.callee, Expression::Identifier(callee) if callee.name == "RegExp")
}
_ => false,
}
}
fn is_prepared_write(expression: &Expression<'_>) -> bool {
let Expression::CallExpression(call) = expression.without_parentheses() else {
return false;
};
let is_prepare = call
.callee
.as_member_expression()
.and_then(|member| member.static_property_name())
.is_some_and(|name| name == PREPARE_METHOD);
is_prepare
&& !first_static_text(call)
.is_some_and(|text| sql_verb(text).eq_ignore_ascii_case(SQL_READ_VERB))
}
#[derive(PartialEq, Eq, Hash)]
enum Receiver {
Name(String),
Field(String),
}
impl Receiver {
fn of(expression: &Expression<'_>) -> Option<Self> {
match expression.without_parentheses() {
Expression::Identifier(name) => Some(Self::Name(name.name.to_string())),
expression => expression.as_member_expression().and_then(Self::of_member),
}
}
fn of_target(target: &AssignmentTarget<'_>) -> Option<Self> {
match target {
AssignmentTarget::AssignmentTargetIdentifier(name) => {
Some(Self::Name(name.name.to_string()))
}
target => target.as_member_expression().and_then(Self::of_member),
}
}
fn of_member(member: &MemberExpression<'_>) -> Option<Self> {
match member {
MemberExpression::PrivateFieldExpression(field) => {
Some(Self::Field(format!("#{}", field.field.name)))
}
member => member
.static_property_name()
.map(|name| Self::Field(name.to_owned())),
}
}
fn of_key(key: &PropertyKey<'_>) -> Option<Self> {
match key {
PropertyKey::PrivateIdentifier(name) => Some(Self::Field(format!("#{}", name.name))),
key => key.static_name().map(|name| Self::Field(name.into_owned())),
}
}
}
struct NamedRun {
start: u32,
receiver: Receiver,
prepared: bool,
}
#[derive(Default)]
struct Writes {
direct: bool,
starts: Vec<u32>,
prepared_receivers: FxHashSet<Receiver>,
regex_names: FxHashSet<String>,
named_runs: Vec<NamedRun>,
}
impl Writes {
fn check_member_call(&mut self, call: &CallExpression<'_>) {
let Some(member) = call.callee.as_member_expression() else {
return;
};
let Some(name) = member.static_property_name() else {
return;
};
let object = member.object().without_parentheses();
let start = call.span.start;
if DATABASE_WRITE_METHODS.contains(&name)
&& !is_runtime_receiver(object)
&& !is_route_path(call)
{
self.starts.push(start);
}
if PREPARED_RUN_METHODS.contains(&name) {
if is_prepared_write(object) {
self.starts.push(start);
} else if let Some(receiver) = Receiver::of(object) {
self.named_runs.push(NamedRun {
start,
receiver,
prepared: true,
});
}
}
if self.direct && STATEMENT_RUN_METHODS.contains(&name) && !is_regex(object) {
match object {
Expression::Identifier(receiver) => self.named_runs.push(NamedRun {
start,
receiver: Receiver::Name(receiver.name.to_string()),
prepared: false,
}),
_ => self.starts.push(start),
}
}
}
fn finish(mut self) -> Vec<u32> {
for run in &self.named_runs {
let write = match (&run.receiver, run.prepared) {
(receiver, true) => self.prepared_receivers.contains(receiver),
(Receiver::Name(name), false) => !self.regex_names.contains(name),
(Receiver::Field(_), false) => true,
};
if write {
self.starts.push(run.start);
}
}
self.starts
}
}
impl<'a> Visit<'a> for Writes {
fn visit_variable_declarator(&mut self, it: &VariableDeclarator<'a>) {
if let (BindingPattern::BindingIdentifier(id), Some(init)) = (&it.id, &it.init) {
if is_prepared_write(init) {
self.prepared_receivers
.insert(Receiver::Name(id.name.to_string()));
} else if is_regex(init) {
self.regex_names.insert(id.name.to_string());
}
}
oxc_ast_visit::walk::walk_variable_declarator(self, it);
}
fn visit_property_definition(&mut self, it: &PropertyDefinition<'a>) {
if let Some(value) = &it.value
&& is_prepared_write(value)
&& let Some(receiver) = Receiver::of_key(&it.key)
{
self.prepared_receivers.insert(receiver);
}
oxc_ast_visit::walk::walk_property_definition(self, it);
}
fn visit_object_property(&mut self, it: &ObjectProperty<'a>) {
if is_prepared_write(&it.value)
&& let Some(receiver) = Receiver::of_key(&it.key)
{
self.prepared_receivers.insert(receiver);
}
oxc_ast_visit::walk::walk_object_property(self, it);
}
fn visit_assignment_expression(&mut self, it: &AssignmentExpression<'a>) {
if is_prepared_write(&it.right)
&& let Some(receiver) = Receiver::of_target(&it.left)
{
self.prepared_receivers.insert(receiver);
}
oxc_ast_visit::walk::walk_assignment_expression(self, it);
}
fn visit_call_expression(&mut self, it: &CallExpression<'a>) {
self.check_member_call(it);
if first_static_text(it).is_some_and(is_sql_write) {
self.starts.push(it.span.start);
}
oxc_ast_visit::walk::walk_call_expression(self, it);
}
fn visit_tagged_template_expression(&mut self, it: &TaggedTemplateExpression<'a>) {
let write_tag = it.tag.as_member_expression().is_some_and(|member| {
member.static_property_name().is_some_and(|name| {
DATABASE_WRITE_METHODS.contains(&name)
|| (self.direct && STATEMENT_RUN_METHODS.contains(&name))
}) && !is_runtime_receiver(member.object())
});
let sql_write = it
.quasi
.quasis
.first()
.is_some_and(|quasi| is_sql_write(&quasi.value.raw));
if write_tag || sql_write {
self.starts.push(it.span.start);
}
oxc_ast_visit::walk::walk_tagged_template_expression(self, it);
}
}