use crate::rule_prelude::*;
use ast::*;
use rslint_parser::TextRange;
use std::borrow::Borrow;
use std::cmp;
use SyntaxKind::*;
pub fn get_assignment_expr_value(expr: AssignExpr) -> std::string::String {
assert!(expr.op().is_some());
let tok = expr.syntax().first_lossy_token().unwrap();
let op_str = tok.text();
if op_str == "=" {
expr.rhs()
.map(|e| e.syntax().trimmed_text().to_string())
.unwrap_or_default()
} else {
format!(
"{} {} {}",
expr.lhs()
.map(|e| e.syntax().trimmed_text().to_string())
.unwrap_or_default(),
op_str[..op_str.len() - 1].to_string(),
expr.rhs()
.map(|e| e.syntax().trimmed_text().to_string())
.unwrap_or_default()
)
}
}
pub fn simple_bool_coerce(condition: Expr) -> Option<bool> {
match condition {
Expr::Literal(lit) => {
let coerced = match lit.kind() {
LiteralKind::Bool(val) => val,
LiteralKind::Null => false,
LiteralKind::Number(num) => num != 0.0,
LiteralKind::BigInt(bigint) => bigint != 0.into(),
LiteralKind::String => !lit.inner_string_text().unwrap().is_empty(),
LiteralKind::Regex => true,
};
Some(coerced)
}
Expr::Template(tpl) => {
if tpl.quasis().any(|t| !t.text().is_empty()) {
Some(true)
} else if tpl.syntax().text().len() == 2.into() {
Some(false)
} else {
None
}
}
Expr::ObjectExpr(_) | Expr::ArrayExpr(_) | Expr::FnExpr(_) => Some(true),
Expr::AssignExpr(assign)
if assign
.op()
.map(|op| op == AssignOp::Assign)
.unwrap_or_default() =>
{
simple_bool_coerce(assign.rhs()?)
}
Expr::SequenceExpr(seqexpr) => simple_bool_coerce(seqexpr.exprs().last()?),
Expr::NameRef(name) => match name.ident_token()?.text().as_str() {
"NaN" | "undefined" => Some(false),
"Infinity" => Some(true),
_ => None,
},
Expr::BinExpr(binexpr) => match binexpr.op()? {
BinOp::LogicalAnd => {
Some(simple_bool_coerce(binexpr.lhs()?)? && simple_bool_coerce(binexpr.rhs()?)?)
}
BinOp::LogicalOr => {
Some(simple_bool_coerce(binexpr.lhs()?)? || simple_bool_coerce(binexpr.rhs()?)?)
}
_ => None,
},
Expr::CondExpr(condexpr) => {
if simple_bool_coerce(condexpr.test()?)? {
simple_bool_coerce(condexpr.cons()?)
} else {
simple_bool_coerce(condexpr.alt()?)
}
}
_ => None,
}
}
pub fn multi_node_range(mut nodes: impl Iterator<Item = SyntaxNode>) -> TextRange {
TextRange::new(
nodes
.next()
.map(|x| x.trimmed_range().start())
.unwrap_or_else(|| 0.into()),
nodes
.last()
.map(|x| x.trimmed_range().end())
.unwrap_or_else(|| 0.into()),
)
}
pub fn is_const_ident(ident: SyntaxToken) -> bool {
["NaN", "Infinity", "undefined"].contains(&&*ident.text().to_string())
}
pub fn is_const(expr: Expr, boolean_pos: bool, notes: &mut Vec<&str>) -> bool {
match expr {
Expr::Literal(_) | Expr::ObjectExpr(_) | Expr::FnExpr(_) | Expr::ArrowExpr(_) => true,
Expr::NameRef(name) => util::is_const_ident(name.ident_token().unwrap()),
Expr::Template(tpl) => {
(boolean_pos && tpl.quasis().any(|t| !t.text().is_empty()))
|| tpl
.elements()
.all(|e| e.expr().map_or(false, |e| is_const(e, boolean_pos, notes)))
}
Expr::GroupingExpr(group) => group
.inner()
.map_or(true, |e| is_const(e, boolean_pos, notes)),
Expr::ArrayExpr(array) => {
let not_const = array.syntax().parent().map_or(false, |p| {
p.kind() == BIN_EXPR && p.to::<BinExpr>().op() == Some(BinOp::Plus)
});
if not_const {
array.elements().all(|elem| {
if let ExprOrSpread::Expr(expr) = elem {
is_const(expr, boolean_pos, notes)
} else {
false
}
})
} else {
true
}
}
Expr::UnaryExpr(unexpr) => {
if unexpr.op() == Some(UnaryOp::Void) {
notes.push("note: void always returns `undefined`, which makes the expression always falsey");
true
} else {
(unexpr.op() == Some(UnaryOp::Typeof) && boolean_pos)
|| unexpr
.expr()
.map_or(true, |e| is_const(e, boolean_pos, notes))
}
}
Expr::BinExpr(binexpr) => {
if binexpr.conditional() {
let lhs_const = binexpr
.lhs()
.map_or(false, |expr| is_const(expr, boolean_pos, notes));
let rhs_const = binexpr
.rhs()
.map_or(false, |expr| is_const(expr, boolean_pos, notes));
let lhs_short_circuits = binexpr.lhs().map_or(false, |expr| {
binexpr.op().map_or(false, |op| short_circuits(expr, op))
});
let rhs_short_circuits = binexpr.rhs().map_or(false, |expr| {
binexpr.op().map_or(false, |op| short_circuits(expr, op))
});
(lhs_const && rhs_const) || lhs_short_circuits || rhs_short_circuits
} else {
let lhs_const = binexpr
.lhs()
.map_or(false, |expr| is_const(expr, false, notes));
let rhs_const = binexpr
.rhs()
.map_or(false, |expr| is_const(expr, false, notes));
lhs_const && rhs_const && binexpr.op() != Some(op!(in))
}
}
Expr::AssignExpr(assignexpr) => {
if assignexpr.op() == Some(AssignOp::Assign) && assignexpr.rhs().is_some() {
is_const(assignexpr.rhs().unwrap(), boolean_pos, notes)
} else {
false
}
}
Expr::SequenceExpr(seqexpr) => {
is_const(seqexpr.exprs().last().unwrap(), boolean_pos, notes)
}
_ => false,
}
}
fn short_circuits(expr: Expr, op: BinOp) -> bool {
match expr {
Expr::Literal(lit) => {
if let LiteralKind::Bool(b) = lit.kind() {
match op {
BinOp::LogicalOr => b,
BinOp::LogicalAnd => !b,
_ => false,
}
} else {
false
}
}
Expr::UnaryExpr(unexpr) => {
op == BinOp::LogicalAnd && unexpr.op().map_or(false, |op| op == UnaryOp::Void)
}
Expr::BinExpr(binexpr) => {
if binexpr.conditional() {
binexpr
.lhs()
.map_or(false, |expr| short_circuits(expr, binexpr.op().unwrap()))
|| binexpr
.rhs()
.map_or(false, |expr| short_circuits(expr, binexpr.op().unwrap()))
} else {
false
}
}
_ => false,
}
}
pub fn simple_const_condition_context(
parent: SyntaxNode,
condition_value: bool,
mut diagnostic: Diagnostic,
) -> Diagnostic {
match parent.kind() {
COND_EXPR => {
let condexpr = parent.to::<CondExpr>();
if condition_value && condexpr.cons().is_some() {
diagnostic = diagnostic
.primary(
condexpr.test().unwrap().syntax().trimmed_range(),
"this expression is always truthy...",
)
.secondary(
condexpr.cons().unwrap().syntax().trimmed_range(),
"...which means this expression is always returned",
);
} else if !condition_value && condexpr.alt().is_some() {
diagnostic = diagnostic
.primary(
condexpr.test().unwrap().syntax().trimmed_range(),
"this expression is always falsey...",
)
.secondary(
condexpr.alt().unwrap().syntax().trimmed_range(),
"...which means this expression is always returned",
);
}
}
IF_STMT => {
let stmt = parent.to::<IfStmt>();
if condition_value {
if let Some(alt) = stmt.alt() {
diagnostic = diagnostic
.primary(
stmt.condition().unwrap().syntax().trimmed_range(),
"this condition is always truthy...",
)
.secondary(
alt.syntax().trimmed_range(),
"...which makes this unreachable",
);
} else if let Some(cons) = stmt.cons() {
diagnostic = diagnostic
.primary(
stmt.condition().unwrap().syntax().trimmed_range(),
"this condition is always truthy...",
)
.secondary(
cons.syntax().trimmed_range(),
"...which makes this always run",
);
} else {
diagnostic = diagnostic.primary(
stmt.condition().unwrap().syntax().trimmed_range(),
"this condition consistently yields one result",
)
}
} else if !condition_value && stmt.alt().is_some() {
diagnostic = diagnostic
.primary(
stmt.condition().unwrap().syntax().trimmed_range(),
"this condition is always falsey...",
)
.secondary(
stmt.alt().unwrap().syntax().trimmed_range(),
"...which makes this unreachable",
);
}
}
WHILE_STMT => {
let stmt = parent.to::<WhileStmt>();
if let Some(cons) = stmt.cons().map(|stmt| stmt.syntax().clone()) {
if condition_value {
diagnostic = diagnostic
.primary(
stmt.condition().unwrap().syntax().trimmed_range(),
"this condition is always truthy...",
)
.secondary(cons.trimmed_range(), "...which makes this infinitely loop");
} else {
diagnostic = diagnostic
.primary(
stmt.condition().unwrap().syntax().trimmed_range(),
"this condition is always falsey...",
)
.secondary(cons.trimmed_range(), "...which makes this loop never run");
}
}
}
DO_WHILE_STMT => {
let stmt = parent.to::<DoWhileStmt>();
if let Some(cons) = stmt.cons().map(|stmt| stmt.syntax().clone()) {
if condition_value {
diagnostic = diagnostic
.primary(
stmt.condition().unwrap().syntax().trimmed_range(),
"this condition is always truthy...",
)
.secondary(cons.trimmed_range(), "...which makes this infinitely loop");
} else {
diagnostic = diagnostic
.primary(
stmt.condition().unwrap().syntax().trimmed_range(),
"this condition is always falsey...",
)
.secondary(
cons.trimmed_range(),
"...which makes this loop only run once",
);
}
}
}
FOR_STMT => {
let stmt = parent.to::<ForStmt>();
if let Some(cons) = stmt.cons().map(|stmt| stmt.syntax().clone()) {
if condition_value {
diagnostic = diagnostic
.primary(
stmt.test().unwrap().syntax().trimmed_range(),
"this test condition is always truthy...",
)
.secondary(cons.trimmed_range(), "...which makes this infinitely loop");
} else {
diagnostic = diagnostic
.primary(
stmt.test().unwrap().syntax().trimmed_range(),
"this test condition is always falsey...",
)
.secondary(cons.trimmed_range(), "...which makes this loop never run");
}
}
}
_ => {}
}
diagnostic
}
pub fn token_list_range<I>(items: I) -> TextRange
where
I: IntoIterator,
I::Item: Borrow<SyntaxToken>,
{
let collection = items
.into_iter()
.map(|x| x.borrow().clone())
.collect::<Vec<_>>();
let start = collection
.first()
.expect("Empty token list")
.text_range()
.start();
let end = collection
.last()
.expect("Empty token list")
.text_range()
.end();
TextRange::new(start, end)
}
pub fn string_token_eq<L, R>(left: L, right: R) -> bool
where
L: IntoIterator,
R: IntoIterator,
L::Item: Borrow<SyntaxToken>,
R::Item: Borrow<SyntaxToken>,
{
let left_vec: Vec<L::Item> = left.into_iter().collect();
let right_vec: Vec<R::Item> = right.into_iter().collect();
if left_vec.len() != right_vec.len() {
return false;
}
left_vec
.into_iter()
.zip(right_vec.into_iter())
.all(|(l, r)| l.borrow().to_string() == r.borrow().to_string())
}
pub fn levenshtein_distance(a: &str, b: &str) -> usize {
if a.is_empty() {
return b.chars().count();
} else if b.is_empty() {
return a.chars().count();
}
let mut dcol: Vec<_> = (0..=b.len()).collect();
let mut t_last = 0;
for (i, sc) in a.chars().enumerate() {
let mut current = i;
dcol[0] = current + 1;
for (j, tc) in b.chars().enumerate() {
let next = dcol[j + 1];
if sc == tc {
dcol[j + 1] = current;
} else {
dcol[j + 1] = cmp::min(current, next);
dcol[j + 1] = cmp::min(dcol[j + 1], dcol[j]) + 1;
}
current = next;
t_last = j;
}
}
dcol[t_last + 1]
}
pub fn find_best_match_for_name<'a>(
iter_names: impl Iterator<Item = &'a str>,
lookup: &str,
dist: impl Into<Option<usize>>,
) -> Option<&'a str> {
let max_dist = dist
.into()
.map_or_else(|| cmp::max(lookup.len(), 3) / 3, |d| d);
let name_vec = iter_names.collect::<Vec<_>>();
let (case_insensitive_match, levenshtein_match) = name_vec
.iter()
.filter_map(|&name| {
let dist = levenshtein_distance(lookup, name);
if dist <= max_dist {
Some((name, dist))
} else {
None
}
})
.fold((None, None), |result, (candidate, dist)| {
(
if candidate.to_uppercase() == lookup.to_uppercase() {
Some(candidate)
} else {
result.0
},
match result.1 {
None => Some((candidate, dist)),
Some((c, d)) => Some(if dist < d { (candidate, dist) } else { (c, d) }),
},
)
});
if let Some(candidate) = case_insensitive_match {
Some(candidate)
} else if levenshtein_match.is_some() {
levenshtein_match.map(|x| x.0)
} else {
find_match_by_sorted_words(name_vec, lookup)
}
}
fn find_match_by_sorted_words<'a>(iter_names: Vec<&'a str>, lookup: &str) -> Option<&'a str> {
iter_names.iter().fold(None, |result, candidate| {
if sort_by_words(&candidate) == sort_by_words(lookup) {
Some(candidate)
} else {
result
}
})
}
fn sort_by_words(name: &str) -> std::string::String {
let mut split_words: Vec<&str> = name.split('_').collect();
split_words.sort_unstable();
split_words.join("_")
}
pub fn constructor_or_call_with_callee(
node: impl Borrow<SyntaxNode>,
name: impl AsRef<str>,
) -> bool {
let node = node.borrow();
match node.kind() {
NEW_EXPR | CALL_EXPR => node.children().any(|child| child.text() == name.as_ref()),
_ => false,
}
}
pub fn outer_function(node: impl Borrow<SyntaxNode>) -> Option<SyntaxNode> {
node.borrow()
.ancestors()
.skip(1)
.find(|ancestor| matches!(ancestor.kind(), ARROW_EXPR | FN_DECL | FN_EXPR))
}