1use crate::{
2 ast_util::scopes::AssignedValue,
3 standard_library::{Field, FieldKind, Observes},
4};
5
6use super::*;
7
8use full_moon::ast::Ast;
9use regex::Regex;
10use serde::Deserialize;
11
12#[derive(Clone, Deserialize)]
13#[serde(default)]
14pub struct UnusedVariableConfig {
15 allow_unused_self: bool,
16 ignore_pattern: String,
17}
18
19impl Default for UnusedVariableConfig {
20 fn default() -> Self {
21 Self {
22 allow_unused_self: true,
23 ignore_pattern: "^_".to_owned(),
24 }
25 }
26}
27
28pub struct UnusedVariableLint {
29 allow_unused_self: bool,
30 ignore_pattern: Regex,
31}
32
33#[derive(Debug, PartialEq, Eq)]
34pub enum AnalyzedReference {
35 Read,
36 PlainWrite,
37 ObservedWrite(Label),
38}
39
40impl Lint for UnusedVariableLint {
41 type Config = UnusedVariableConfig;
42 type Error = regex::Error;
43
44 const SEVERITY: Severity = Severity::Warning;
45 const LINT_TYPE: LintType = LintType::Style;
46
47 fn new(config: Self::Config) -> Result<Self, Self::Error> {
48 Ok(Self {
49 allow_unused_self: config.allow_unused_self,
50 ignore_pattern: Regex::new(&config.ignore_pattern)?,
51 })
52 }
53
54 fn pass(&self, _: &Ast, context: &Context, ast_context: &AstContext) -> Vec<Diagnostic> {
55 let mut diagnostics = Vec::new();
56
57 for (_, variable) in ast_context
58 .scope_manager
59 .variables
60 .iter()
61 .filter(|(_, variable)| !self.ignore_pattern.is_match(&variable.name))
62 {
63 if context.standard_library.global_has_fields(&variable.name) {
64 continue;
65 }
66
67 let references = variable
68 .references
69 .iter()
70 .copied()
71 .map(|id| &ast_context.scope_manager.references[id]);
72
73 let analyzed_references = references
75 .map(|reference| {
76 let is_static_table =
77 matches!(variable.value, Some(AssignedValue::StaticTable { .. }));
78
79 if reference.write.is_some() {
80 if let Some(indexing) = &reference.indexing {
81 if is_static_table
82 && indexing.len() == 1 && indexing.iter().any(|index| index.static_name.is_some())
84 {
85 return AnalyzedReference::ObservedWrite(Label::new_with_message(
86 reference.identifier,
87 format!("`{}` is only getting written to", variable.name),
88 ));
89 }
90 }
91
92 if !reference.read {
93 return AnalyzedReference::PlainWrite;
94 }
95 }
96
97 if !is_static_table {
98 return AnalyzedReference::Read;
99 }
100
101 let within_function_stmt = match &reference.within_function_stmt {
102 Some(within_function_stmt) => within_function_stmt,
103 None => return AnalyzedReference::Read,
104 };
105
106 let function_call_stmt = &ast_context.scope_manager.function_calls
107 [within_function_stmt.function_call_stmt_id];
108
109 if ast_context.scope_manager.references[function_call_stmt.initial_reference]
111 .resolved
112 .is_some()
113 {
114 return AnalyzedReference::Read;
115 }
116
117 let function_behavior = match context
118 .standard_library
119 .find_global(&function_call_stmt.call_name_path)
120 {
121 Some(Field {
122 field_kind: FieldKind::Function(function_behavior),
123 ..
124 }) => function_behavior,
125 _ => return AnalyzedReference::Read,
126 };
127
128 let argument = match function_behavior
129 .arguments
130 .get(within_function_stmt.argument_index)
131 {
132 Some(argument) => argument,
133 None => return AnalyzedReference::Read,
134 };
135
136 let write_only = argument.observes == Observes::Write;
137
138 if !write_only {
139 return AnalyzedReference::Read;
140 }
141
142 AnalyzedReference::ObservedWrite(Label::new_with_message(
143 reference.identifier,
144 format!(
145 "`{}` only writes to `{}`",
146 function_call_stmt.call_name_path.join("."),
148 variable.name
149 ),
150 ))
151 })
152 .collect::<Vec<_>>();
153
154 if !analyzed_references
155 .iter()
156 .any(|reference| reference == &AnalyzedReference::Read)
157 {
158 let mut notes = Vec::new();
159
160 if variable.is_self {
161 if self.allow_unused_self {
162 continue;
163 }
164
165 notes.push("`self` is implicitly defined when defining a method".to_owned());
166 notes
167 .push("if you don't need it, consider using `.` instead of `:`".to_owned());
168 }
169
170 let write_only = !analyzed_references.is_empty();
171
172 diagnostics.push(Diagnostic::new_complete(
173 "unused_variable",
174 if write_only {
175 format!("{} is assigned a value, but never used", variable.name)
176 } else {
177 format!("{} is defined, but never used", variable.name)
178 },
179 Label::new(variable.identifiers[0]),
180 notes,
181 analyzed_references
182 .into_iter()
183 .filter_map(|reference| {
184 if let AnalyzedReference::ObservedWrite(label) = reference {
185 Some(label)
186 } else {
187 None
188 }
189 })
190 .collect(),
191 ));
192 };
193 }
194
195 diagnostics
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::{
202 super::test_util::{test_lint, test_lint_config, TestUtilConfig},
203 *,
204 };
205
206 #[cfg(feature = "roblox")]
207 #[test]
208 fn test_attributes() {
209 test_lint_config(
210 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
211 "unused_variable",
212 "attributes",
213 TestUtilConfig::luau(),
214 );
215 }
216
217 #[test]
218 fn test_blocks() {
219 test_lint(
220 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
221 "unused_variable",
222 "blocks",
223 );
224 }
225
226 #[cfg(feature = "roblox")]
227 #[test]
228 fn test_consts() {
229 test_lint_config(
230 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
231 "unused_variable",
232 "consts",
233 TestUtilConfig::luau(),
234 );
235 }
236
237 #[test]
238 fn test_locals() {
239 test_lint(
240 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
241 "unused_variable",
242 "locals",
243 );
244 }
245
246 #[test]
247 fn test_edge_cases() {
248 test_lint(
249 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
250 "unused_variable",
251 "edge_cases",
252 );
253 }
254
255 #[test]
256 fn test_explicit_self() {
257 test_lint(
258 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
259 "unused_variable",
260 "explicit_self",
261 );
262 }
263
264 #[test]
265 fn test_function_overriding() {
266 test_lint(
267 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
268 "unused_variable",
269 "function_overriding",
270 );
271 }
272
273 #[test]
274 fn test_generic_for_shadowing() {
275 test_lint(
276 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
277 "unused_variable",
278 "generic_for_shadowing",
279 );
280 }
281
282 #[test]
283 fn test_if() {
284 test_lint(
285 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
286 "unused_variable",
287 "if",
288 );
289 }
290
291 #[test]
292 fn test_ignore() {
293 test_lint(
294 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
295 "unused_variable",
296 "ignore",
297 );
298 }
299
300 #[test]
301 fn test_invalid_regex() {
302 assert!(UnusedVariableLint::new(UnusedVariableConfig {
303 ignore_pattern: "(".to_owned(),
304 ..UnusedVariableConfig::default()
305 })
306 .is_err());
307 }
308
309 #[test]
310 fn test_mutating_functions() {
311 test_lint(
312 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
313 "unused_variable",
314 "mutating_functions",
315 );
316 }
317
318 #[test]
319 fn test_observes() {
320 test_lint(
321 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
322 "unused_variable",
323 "observes",
324 );
325 }
326
327 #[test]
328 fn test_overriding() {
329 test_lint(
330 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
331 "unused_variable",
332 "overriding",
333 );
334 }
335
336 #[test]
337 fn test_self() {
338 test_lint(
339 UnusedVariableLint::new(UnusedVariableConfig {
340 allow_unused_self: false,
341 ..UnusedVariableConfig::default()
342 })
343 .unwrap(),
344 "unused_variable",
345 "self",
346 );
347 }
348
349 #[test]
350 fn test_self_ignored() {
351 test_lint(
352 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
353 "unused_variable",
354 "self_ignored",
355 );
356 }
357
358 #[test]
359 fn test_shadowing() {
360 test_lint(
361 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
362 "unused_variable",
363 "shadowing",
364 );
365 }
366
367 #[test]
368 fn test_varargs() {
369 test_lint(
370 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
371 "unused_variable",
372 "varargs",
373 );
374 }
375
376 #[cfg(feature = "roblox")]
377 #[test]
378 fn test_types() {
379 test_lint_config(
380 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
381 "unused_variable",
382 "types",
383 TestUtilConfig::luau(),
384 );
385 }
386
387 #[cfg(feature = "roblox")]
388 #[test]
389 fn test_types_generic_instantiation() {
390 test_lint_config(
391 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
392 "unused_variable",
393 "types_generic_instantiation",
394 TestUtilConfig::luau(),
395 );
396 }
397
398 #[test]
399 fn test_write_only() {
400 test_lint(
401 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
402 "unused_variable",
403 "write_only",
404 );
405 }
406}