1use full_moon::{
2 ast::{self, punctuated::Punctuated, Expression},
3 visitors::Visitor,
4};
5
6use crate::ast_util::{expression_to_ident, range, scopes::AssignedValue, strip_parentheses};
7
8use super::*;
9use std::{collections::HashSet, convert::Infallible};
10
11pub struct ManualTableCloneLint;
12
13impl Lint for ManualTableCloneLint {
14 type Config = ();
15 type Error = Infallible;
16
17 const SEVERITY: Severity = Severity::Warning;
18 const LINT_TYPE: LintType = LintType::Complexity;
19
20 fn new(_: Self::Config) -> Result<Self, Self::Error> {
21 Ok(ManualTableCloneLint)
22 }
23
24 fn pass(&self, ast: &Ast, context: &Context, ast_context: &AstContext) -> Vec<Diagnostic> {
25 if context
26 .standard_library
27 .find_global(&["table", "clone"])
28 .is_none()
29 {
30 return Vec::new();
31 }
32
33 let mut visitor = ManualTableCloneVisitor {
34 matches: Vec::new(),
35 scope_manager: &ast_context.scope_manager,
36 completed_stmt_begins: Vec::new(),
37 inside_stmt_begins: HashSet::new(),
38 };
39
40 visitor.visit_ast(ast);
41
42 visitor
43 .matches
44 .into_iter()
45 .map(ManualTableCloneMatch::into_diagnostic)
46 .collect()
47 }
48}
49
50#[derive(Debug)]
51struct ManualTableCloneMatch {
52 range: (usize, usize),
53 assigning_into: String,
54 looping_over: String,
55 loop_type: LoopType,
56 replaces_definition_range: Option<(usize, usize)>,
57}
58
59impl ManualTableCloneMatch {
60 fn into_diagnostic(self) -> Diagnostic {
61 Diagnostic::new_complete(
62 "manual_table_clone",
63 "manual implementation of table.clone".to_owned(),
64 Label::new(self.range),
65 {
66 let mut notes = vec![format!(
67 "try `local {} = table.clone({})`",
68 self.assigning_into.trim(),
69 self.looping_over.trim()
70 )];
71
72 if matches!(self.loop_type, LoopType::Ipairs) {
73 notes.push("if this is a mixed table, then table.clone is not equivalent, as ipairs only goes over the array portion.\n\
74 ignore this lint with `-- selene: allow(manual_table_clone)` if this is the case.".to_owned());
75 }
76
77 notes
78 },
79 if let Some(definition_range) = self.replaces_definition_range {
80 vec![Label::new_with_message(
81 definition_range,
82 "remove this definition".to_owned(),
83 )]
84 } else {
85 Vec::new()
86 },
87 )
88 }
89}
90
91struct ManualTableCloneVisitor<'ast> {
92 matches: Vec<ManualTableCloneMatch>,
93 scope_manager: &'ast ScopeManager,
94 completed_stmt_begins: Vec<usize>,
95 inside_stmt_begins: HashSet<usize>,
96}
97
98#[derive(Debug)]
99enum LoopType {
100 Ipairs,
101 Other,
102}
103
104impl ManualTableCloneVisitor<'_> {
105 fn is_luau(&self) -> bool {
108 true
109 }
110
111 fn loop_expression<'ast>(
112 &self,
113 expressions: &'ast Punctuated<Expression>,
114 ) -> Option<(LoopType, &'ast Expression)> {
115 match expressions.len() {
116 1 => {
117 let loop_expression = expressions.iter().next().unwrap();
118
119 let function_call = match strip_parentheses(loop_expression) {
120 Expression::FunctionCall(function_call) => function_call,
121 _ if self.is_luau() => return Some((LoopType::Other, loop_expression)),
122 _ => return None,
123 };
124
125 #[cfg_attr(
126 feature = "force_exhaustive_checks",
127 deny(non_exhaustive_omitted_patterns)
128 )]
129 let function_token = match function_call.prefix() {
130 ast::Prefix::Name(name) => name,
131 ast::Prefix::Expression(expression) => expression_to_ident(expression)?,
132 _ => return None,
133 };
134
135 let function_name = function_token.token().to_string();
136 let looping_expression;
137
138 if function_name == "ipairs" || function_name == "pairs" {
139 let suffixes = function_call.suffixes().collect::<Vec<_>>();
140 if suffixes.len() != 1 {
141 return None;
142 }
143
144 let suffix = suffixes[0];
145
146 let inner_expression = match suffix {
147 ast::Suffix::Call(ast::Call::AnonymousCall(
148 ast::FunctionArgs::Parentheses { arguments, .. },
149 )) => {
150 if arguments.len() != 1 {
151 return None;
152 }
153
154 arguments.iter().next().unwrap()
155 }
156
157 _ => return None,
158 };
159
160 looping_expression = inner_expression;
161 } else if self.is_luau() {
162 looping_expression = loop_expression;
164 } else {
165 return None;
166 }
167
168 Some((
169 if function_name == "ipairs" {
170 LoopType::Ipairs
171 } else {
172 LoopType::Other
173 },
174 looping_expression,
175 ))
176 }
177
178 2 => {
179 let mut expressions = expressions.iter();
180 let (first, second) = (expressions.next().unwrap(), expressions.next().unwrap());
181
182 match expression_to_ident(first) {
183 Some(ident) => {
184 if ident.token().to_string() != "next" {
185 return None;
186 }
187 }
188
189 _ => return None,
190 }
191
192 Some((LoopType::Other, second))
193 }
194
195 _ => None,
196 }
197 }
198
199 fn statement_in_way_of_definition(
200 &self,
201 definition_end: usize,
202 assigment_start: usize,
203 ) -> bool {
204 debug_assert!(assigment_start > definition_end);
205
206 for &stmt_begin in self.completed_stmt_begins.iter() {
207 if stmt_begin > definition_end {
208 return true;
209 } else if stmt_begin > assigment_start {
210 return false;
211 }
212 }
213
214 false
215 }
216
217 fn get_depth_at_byte(&self, byte: usize) -> usize {
218 self.inside_stmt_begins
219 .iter()
220 .filter(|&&start| start < byte)
221 .count()
222 }
223}
224
225fn has_filter_comment(for_loop: &ast::GenericFor) -> bool {
226 let (leading_trivia, ..) = for_loop.surrounding_trivia();
227
228 for trivia in leading_trivia {
229 let comment = match trivia.token_type() {
230 full_moon::tokenizer::TokenType::SingleLineComment { comment } => comment,
231 full_moon::tokenizer::TokenType::MultiLineComment { comment, .. } => comment,
232 _ => continue,
233 };
234
235 let filters = match crate::lint_filtering::parse_comment(comment.trim()) {
236 Some(filters) => filters,
237 None => continue,
238 };
239
240 if filters
241 .into_iter()
242 .any(|filter| filter.lint == "manual_table_clone")
243 {
244 return true;
245 }
246 }
247
248 false
249}
250
251impl Visitor for ManualTableCloneVisitor<'_> {
252 fn visit_generic_for(&mut self, node: &ast::GenericFor) {
253 let (loop_type, looping_over) = match self.loop_expression(node.expressions()) {
254 Some(loop_expression) => loop_expression,
255 None => return,
256 };
257
258 let names = node.names().iter().collect::<Vec<_>>();
259 if names.len() != 2 {
260 return;
261 }
262
263 let (key, value) = (names[0].token().to_string(), names[1].token().to_string());
264
265 let statements = node.block().stmts().collect::<Vec<_>>();
266
267 if statements.len() != 1 {
268 return;
269 }
270
271 let assignment = match statements[0] {
272 ast::Stmt::Assignment(assignment) => assignment,
273 _ => return,
274 };
275
276 let variables = assignment.variables();
277 if variables.len() != 1 {
278 return;
279 }
280
281 let variable = variables.iter().next().unwrap();
282
283 let assigning_into = match variable {
284 ast::Var::Expression(var_expression) => {
285 let name = match var_expression.prefix() {
286 ast::Prefix::Expression(expression) => match expression_to_ident(expression) {
287 Some(name) => name,
288 None => return,
289 },
290 ast::Prefix::Name(name) => name,
291 _ => return,
292 };
293
294 let suffixes = var_expression.suffixes().collect::<Vec<_>>();
295 if suffixes.len() != 1 {
296 return;
297 }
298
299 let index = match &suffixes[0] {
300 ast::Suffix::Index(ast::Index::Brackets { expression, .. }) => expression,
301 _ => return,
302 };
303
304 if expression_to_ident(index).map(|ident| ident.token().to_string()) != Some(key) {
305 return;
306 }
307
308 match assignment
309 .expressions()
310 .iter()
311 .next()
312 .and_then(expression_to_ident)
313 {
314 Some(name) => {
315 if name.token().to_string() != value {
316 return;
317 }
318 }
319
320 _ => return,
321 };
322
323 name
324 }
325
326 _ => return,
327 };
328
329 let (definition_start, definition_end) = match self
330 .scope_manager
331 .reference_at_byte(assigning_into.token().start_position().bytes())
332 {
333 Some(reference) => {
334 if reference.resolved.is_none() {
335 return;
336 }
337
338 let variable = &self.scope_manager.variables[reference.resolved.unwrap()];
339
340 if !matches!(
341 variable.value,
342 Some(AssignedValue::StaticTable { has_fields: false })
343 ) {
344 return;
345 }
346
347 let first_definition_range = match variable.definitions.first() {
348 Some(first_definition_range) => first_definition_range,
349 None => {
350 return;
351 }
352 };
353
354 for reference_id in &variable.references {
356 let reference = &self.scope_manager.references[*reference_id];
357
358 if reference.identifier.1 > first_definition_range.1
359 && reference.identifier.0 < assigning_into.token().start_position().bytes()
360 {
361 return;
362 }
363 }
364
365 first_definition_range
366 }
367
368 _ => return,
369 };
370
371 let (position_start, position_end) = range(node);
372
373 if self.get_depth_at_byte(*definition_start) != self.get_depth_at_byte(position_start) {
374 return;
375 }
376
377 let only_use_loop_range = self
378 .statement_in_way_of_definition(*definition_end, position_start)
379 || has_filter_comment(node);
380
381 self.matches.push(ManualTableCloneMatch {
382 range: if only_use_loop_range {
383 (position_start, position_end)
384 } else {
385 (*definition_start, position_end)
386 },
387 assigning_into: assigning_into.token().to_string(),
388 looping_over: looping_over.to_string(),
389 replaces_definition_range: if only_use_loop_range {
390 Some((*definition_start, *definition_end))
391 } else {
392 None
393 },
394 loop_type,
395 });
396 }
397
398 fn visit_stmt(&mut self, stmt: &ast::Stmt) {
399 self.inside_stmt_begins.insert(range(stmt).0);
400 }
401
402 fn visit_stmt_end(&mut self, stmt: &ast::Stmt) {
403 self.completed_stmt_begins.push(range(stmt).0);
404 self.inside_stmt_begins.remove(&range(stmt).0);
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use crate::lints::test_util::{test_lint_config_with_output, TestUtilConfig};
411
412 use super::{super::test_util::test_lint_config, *};
413
414 #[test]
415 fn test_manual_table_clone() {
416 test_lint_config(
417 ManualTableCloneLint::new(()).unwrap(),
418 "manual_table_clone",
419 "manual_table_clone",
420 TestUtilConfig {
421 standard_library: StandardLibrary::from_name("luau").unwrap(),
422 ..Default::default()
423 },
424 );
425 }
426
427 #[test]
428 fn test_no_table_clone() {
429 test_lint_config_with_output(
430 ManualTableCloneLint::new(()).unwrap(),
431 "manual_table_clone",
432 "manual_table_clone",
433 TestUtilConfig::default(),
434 "no_table_clone.stderr",
435 );
436 }
437
438 #[test]
439 fn test_false_positive() {
440 test_lint_config(
441 ManualTableCloneLint::new(()).unwrap(),
442 "manual_table_clone",
443 "false_positive",
444 TestUtilConfig {
445 standard_library: StandardLibrary::from_name("luau").unwrap(),
446 ..Default::default()
447 },
448 )
449 }
450}