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 #[test]
227 fn test_locals() {
228 test_lint(
229 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
230 "unused_variable",
231 "locals",
232 );
233 }
234
235 #[test]
236 fn test_edge_cases() {
237 test_lint(
238 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
239 "unused_variable",
240 "edge_cases",
241 );
242 }
243
244 #[test]
245 fn test_explicit_self() {
246 test_lint(
247 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
248 "unused_variable",
249 "explicit_self",
250 );
251 }
252
253 #[test]
254 fn test_function_overriding() {
255 test_lint(
256 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
257 "unused_variable",
258 "function_overriding",
259 );
260 }
261
262 #[test]
263 fn test_generic_for_shadowing() {
264 test_lint(
265 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
266 "unused_variable",
267 "generic_for_shadowing",
268 );
269 }
270
271 #[test]
272 fn test_if() {
273 test_lint(
274 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
275 "unused_variable",
276 "if",
277 );
278 }
279
280 #[test]
281 fn test_ignore() {
282 test_lint(
283 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
284 "unused_variable",
285 "ignore",
286 );
287 }
288
289 #[test]
290 fn test_invalid_regex() {
291 assert!(UnusedVariableLint::new(UnusedVariableConfig {
292 ignore_pattern: "(".to_owned(),
293 ..UnusedVariableConfig::default()
294 })
295 .is_err());
296 }
297
298 #[test]
299 fn test_mutating_functions() {
300 test_lint(
301 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
302 "unused_variable",
303 "mutating_functions",
304 );
305 }
306
307 #[test]
308 fn test_observes() {
309 test_lint(
310 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
311 "unused_variable",
312 "observes",
313 );
314 }
315
316 #[test]
317 fn test_overriding() {
318 test_lint(
319 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
320 "unused_variable",
321 "overriding",
322 );
323 }
324
325 #[test]
326 fn test_self() {
327 test_lint(
328 UnusedVariableLint::new(UnusedVariableConfig {
329 allow_unused_self: false,
330 ..UnusedVariableConfig::default()
331 })
332 .unwrap(),
333 "unused_variable",
334 "self",
335 );
336 }
337
338 #[test]
339 fn test_self_ignored() {
340 test_lint(
341 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
342 "unused_variable",
343 "self_ignored",
344 );
345 }
346
347 #[test]
348 fn test_shadowing() {
349 test_lint(
350 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
351 "unused_variable",
352 "shadowing",
353 );
354 }
355
356 #[test]
357 fn test_varargs() {
358 test_lint(
359 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
360 "unused_variable",
361 "varargs",
362 );
363 }
364
365 #[cfg(feature = "roblox")]
366 #[test]
367 fn test_types() {
368 test_lint_config(
369 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
370 "unused_variable",
371 "types",
372 TestUtilConfig::luau(),
373 );
374 }
375
376 #[cfg(feature = "roblox")]
377 #[test]
378 fn test_types_generic_instantiation() {
379 test_lint_config(
380 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
381 "unused_variable",
382 "types_generic_instantiation",
383 TestUtilConfig::luau(),
384 );
385 }
386
387 #[test]
388 fn test_write_only() {
389 test_lint(
390 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
391 "unused_variable",
392 "write_only",
393 );
394 }
395}