use full_moon::{
ast::{self, punctuated::Punctuated, Expression},
visitors::Visitor,
};
use crate::ast_util::{expression_to_ident, range, scopes::AssignedValue, strip_parentheses};
use super::*;
use std::{collections::HashSet, convert::Infallible};
pub struct ManualTableCloneLint;
impl Lint for ManualTableCloneLint {
type Config = ();
type Error = Infallible;
const SEVERITY: Severity = Severity::Warning;
const LINT_TYPE: LintType = LintType::Complexity;
fn new(_: Self::Config) -> Result<Self, Self::Error> {
Ok(ManualTableCloneLint)
}
fn pass(&self, ast: &Ast, context: &Context, ast_context: &AstContext) -> Vec<Diagnostic> {
if context
.standard_library
.find_global(&["table", "clone"])
.is_none()
{
return Vec::new();
}
let mut visitor = ManualTableCloneVisitor {
matches: Vec::new(),
scope_manager: &ast_context.scope_manager,
completed_stmt_begins: Vec::new(),
inside_stmt_begins: HashSet::new(),
};
visitor.visit_ast(ast);
visitor
.matches
.into_iter()
.map(ManualTableCloneMatch::into_diagnostic)
.collect()
}
}
#[derive(Debug)]
struct ManualTableCloneMatch {
range: (usize, usize),
assigning_into: String,
looping_over: String,
loop_type: LoopType,
replaces_definition_range: Option<(usize, usize)>,
}
impl ManualTableCloneMatch {
fn into_diagnostic(self) -> Diagnostic {
Diagnostic::new_complete(
"manual_table_clone",
"manual implementation of table.clone".to_owned(),
Label::new(self.range),
{
let mut notes = vec![format!(
"try `local {} = table.clone({})`",
self.assigning_into.trim(),
self.looping_over.trim()
)];
if matches!(self.loop_type, LoopType::Ipairs) {
notes.push("if this is a mixed table, then table.clone is not equivalent, as ipairs only goes over the array portion.\n\
ignore this lint with `-- selene: allow(manual_table_clone)` if this is the case.".to_owned());
}
notes
},
if let Some(definition_range) = self.replaces_definition_range {
vec![Label::new_with_message(
definition_range,
"remove this definition".to_owned(),
)]
} else {
Vec::new()
},
)
}
}
struct ManualTableCloneVisitor<'ast> {
matches: Vec<ManualTableCloneMatch>,
scope_manager: &'ast ScopeManager,
completed_stmt_begins: Vec<usize>,
inside_stmt_begins: HashSet<usize>,
}
#[derive(Debug)]
enum LoopType {
Ipairs,
Other,
}
impl ManualTableCloneVisitor<'_> {
fn is_luau(&self) -> bool {
true
}
fn loop_expression<'ast>(
&self,
expressions: &'ast Punctuated<Expression>,
) -> Option<(LoopType, &'ast Expression)> {
match expressions.len() {
1 => {
let loop_expression = expressions.iter().next().unwrap();
let function_call = match strip_parentheses(loop_expression) {
Expression::FunctionCall(function_call) => function_call,
_ if self.is_luau() => return Some((LoopType::Other, loop_expression)),
_ => return None,
};
#[cfg_attr(
feature = "force_exhaustive_checks",
deny(non_exhaustive_omitted_patterns)
)]
let function_token = match function_call.prefix() {
ast::Prefix::Name(name) => name,
ast::Prefix::Expression(expression) => expression_to_ident(expression)?,
_ => return None,
};
let function_name = function_token.token().to_string();
let looping_expression;
if function_name == "ipairs" || function_name == "pairs" {
let suffixes = function_call.suffixes().collect::<Vec<_>>();
if suffixes.len() != 1 {
return None;
}
let suffix = suffixes[0];
let inner_expression = match suffix {
ast::Suffix::Call(ast::Call::AnonymousCall(
ast::FunctionArgs::Parentheses { arguments, .. },
)) => {
if arguments.len() != 1 {
return None;
}
arguments.iter().next().unwrap()
}
_ => return None,
};
looping_expression = inner_expression;
} else if self.is_luau() {
looping_expression = loop_expression;
} else {
return None;
}
Some((
if function_name == "ipairs" {
LoopType::Ipairs
} else {
LoopType::Other
},
looping_expression,
))
}
2 => {
let mut expressions = expressions.iter();
let (first, second) = (expressions.next().unwrap(), expressions.next().unwrap());
match expression_to_ident(first) {
Some(ident) => {
if ident.token().to_string() != "next" {
return None;
}
}
_ => return None,
}
Some((LoopType::Other, second))
}
_ => None,
}
}
fn statement_in_way_of_definition(
&self,
definition_end: usize,
assigment_start: usize,
) -> bool {
debug_assert!(assigment_start > definition_end);
for &stmt_begin in self.completed_stmt_begins.iter() {
if stmt_begin > definition_end {
return true;
} else if stmt_begin > assigment_start {
return false;
}
}
false
}
fn get_depth_at_byte(&self, byte: usize) -> usize {
self.inside_stmt_begins
.iter()
.filter(|&&start| start < byte)
.count()
}
}
fn has_filter_comment(for_loop: &ast::GenericFor) -> bool {
let (leading_trivia, ..) = for_loop.surrounding_trivia();
for trivia in leading_trivia {
let comment = match trivia.token_type() {
full_moon::tokenizer::TokenType::SingleLineComment { comment } => comment,
full_moon::tokenizer::TokenType::MultiLineComment { comment, .. } => comment,
_ => continue,
};
let filters = match crate::lint_filtering::parse_comment(comment.trim()) {
Some(filters) => filters,
None => continue,
};
if filters
.into_iter()
.any(|filter| filter.lint == "manual_table_clone")
{
return true;
}
}
false
}
impl Visitor for ManualTableCloneVisitor<'_> {
fn visit_generic_for(&mut self, node: &ast::GenericFor) {
let (loop_type, looping_over) = match self.loop_expression(node.expressions()) {
Some(loop_expression) => loop_expression,
None => return,
};
let names = node.names().iter().collect::<Vec<_>>();
if names.len() != 2 {
return;
}
let (key, value) = (names[0].token().to_string(), names[1].token().to_string());
let statements = node.block().stmts().collect::<Vec<_>>();
if statements.len() != 1 {
return;
}
let assignment = match statements[0] {
ast::Stmt::Assignment(assignment) => assignment,
_ => return,
};
let variables = assignment.variables();
if variables.len() != 1 {
return;
}
let variable = variables.iter().next().unwrap();
let assigning_into = match variable {
ast::Var::Expression(var_expression) => {
let name = match var_expression.prefix() {
ast::Prefix::Expression(expression) => match expression_to_ident(expression) {
Some(name) => name,
None => return,
},
ast::Prefix::Name(name) => name,
_ => return,
};
let suffixes = var_expression.suffixes().collect::<Vec<_>>();
if suffixes.len() != 1 {
return;
}
let index = match &suffixes[0] {
ast::Suffix::Index(ast::Index::Brackets { expression, .. }) => expression,
_ => return,
};
if expression_to_ident(index).map(|ident| ident.token().to_string()) != Some(key) {
return;
}
match assignment
.expressions()
.iter()
.next()
.and_then(expression_to_ident)
{
Some(name) => {
if name.token().to_string() != value {
return;
}
}
_ => return,
};
name
}
_ => return,
};
let (definition_start, definition_end) = match self
.scope_manager
.reference_at_byte(assigning_into.token().start_position().bytes())
{
Some(reference) => {
if reference.resolved.is_none() {
return;
}
let variable = &self.scope_manager.variables[reference.resolved.unwrap()];
if !matches!(
variable.value,
Some(AssignedValue::StaticTable { has_fields: false })
) {
return;
}
let first_definition_range = match variable.definitions.first() {
Some(first_definition_range) => first_definition_range,
None => {
return;
}
};
for reference_id in &variable.references {
let reference = &self.scope_manager.references[*reference_id];
if reference.identifier.1 > first_definition_range.1
&& reference.identifier.0 < assigning_into.token().start_position().bytes()
{
return;
}
}
first_definition_range
}
_ => return,
};
let (position_start, position_end) = range(node);
if self.get_depth_at_byte(*definition_start) != self.get_depth_at_byte(position_start) {
return;
}
let only_use_loop_range = self
.statement_in_way_of_definition(*definition_end, position_start)
|| has_filter_comment(node);
self.matches.push(ManualTableCloneMatch {
range: if only_use_loop_range {
(position_start, position_end)
} else {
(*definition_start, position_end)
},
assigning_into: assigning_into.token().to_string(),
looping_over: looping_over.to_string(),
replaces_definition_range: if only_use_loop_range {
Some((*definition_start, *definition_end))
} else {
None
},
loop_type,
});
}
fn visit_stmt(&mut self, stmt: &ast::Stmt) {
self.inside_stmt_begins.insert(range(stmt).0);
}
fn visit_stmt_end(&mut self, stmt: &ast::Stmt) {
self.completed_stmt_begins.push(range(stmt).0);
self.inside_stmt_begins.remove(&range(stmt).0);
}
}
#[cfg(test)]
mod tests {
use crate::lints::test_util::{test_lint_config_with_output, TestUtilConfig};
use super::{super::test_util::test_lint_config, *};
#[test]
fn test_manual_table_clone() {
test_lint_config(
ManualTableCloneLint::new(()).unwrap(),
"manual_table_clone",
"manual_table_clone",
TestUtilConfig {
standard_library: StandardLibrary::from_name("luau").unwrap(),
..Default::default()
},
);
}
#[test]
fn test_no_table_clone() {
test_lint_config_with_output(
ManualTableCloneLint::new(()).unwrap(),
"manual_table_clone",
"manual_table_clone",
TestUtilConfig::default(),
"no_table_clone.stderr",
);
}
#[test]
fn test_false_positive() {
test_lint_config(
ManualTableCloneLint::new(()).unwrap(),
"manual_table_clone",
"false_positive",
TestUtilConfig {
standard_library: StandardLibrary::from_name("luau").unwrap(),
..Default::default()
},
)
}
}