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::{super::test_util::test_lint, *};
202
203 #[test]
204 fn test_blocks() {
205 test_lint(
206 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
207 "unused_variable",
208 "blocks",
209 );
210 }
211
212 #[test]
213 fn test_locals() {
214 test_lint(
215 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
216 "unused_variable",
217 "locals",
218 );
219 }
220
221 #[test]
222 fn test_edge_cases() {
223 test_lint(
224 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
225 "unused_variable",
226 "edge_cases",
227 );
228 }
229
230 #[test]
231 fn test_explicit_self() {
232 test_lint(
233 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
234 "unused_variable",
235 "explicit_self",
236 );
237 }
238
239 #[test]
240 fn test_function_overriding() {
241 test_lint(
242 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
243 "unused_variable",
244 "function_overriding",
245 );
246 }
247
248 #[test]
249 fn test_generic_for_shadowing() {
250 test_lint(
251 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
252 "unused_variable",
253 "generic_for_shadowing",
254 );
255 }
256
257 #[test]
258 fn test_if() {
259 test_lint(
260 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
261 "unused_variable",
262 "if",
263 );
264 }
265
266 #[test]
267 fn test_ignore() {
268 test_lint(
269 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
270 "unused_variable",
271 "ignore",
272 );
273 }
274
275 #[test]
276 fn test_invalid_regex() {
277 assert!(UnusedVariableLint::new(UnusedVariableConfig {
278 ignore_pattern: "(".to_owned(),
279 ..UnusedVariableConfig::default()
280 })
281 .is_err());
282 }
283
284 #[test]
285 fn test_mutating_functions() {
286 test_lint(
287 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
288 "unused_variable",
289 "mutating_functions",
290 );
291 }
292
293 #[test]
294 fn test_observes() {
295 test_lint(
296 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
297 "unused_variable",
298 "observes",
299 );
300 }
301
302 #[test]
303 fn test_overriding() {
304 test_lint(
305 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
306 "unused_variable",
307 "overriding",
308 );
309 }
310
311 #[test]
312 fn test_self() {
313 test_lint(
314 UnusedVariableLint::new(UnusedVariableConfig {
315 allow_unused_self: false,
316 ..UnusedVariableConfig::default()
317 })
318 .unwrap(),
319 "unused_variable",
320 "self",
321 );
322 }
323
324 #[test]
325 fn test_self_ignored() {
326 test_lint(
327 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
328 "unused_variable",
329 "self_ignored",
330 );
331 }
332
333 #[test]
334 fn test_shadowing() {
335 test_lint(
336 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
337 "unused_variable",
338 "shadowing",
339 );
340 }
341
342 #[test]
343 fn test_varargs() {
344 test_lint(
345 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
346 "unused_variable",
347 "varargs",
348 );
349 }
350
351 #[cfg(feature = "roblox")]
352 #[test]
353 fn test_types() {
354 test_lint(
355 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
356 "unused_variable",
357 "types",
358 );
359 }
360
361 #[test]
362 fn test_write_only() {
363 test_lint(
364 UnusedVariableLint::new(UnusedVariableConfig::default()).unwrap(),
365 "unused_variable",
366 "write_only",
367 );
368 }
369}