selene_lib/lints/
unbalanced_assignments.rs

1use super::*;
2use std::convert::Infallible;
3
4use full_moon::{
5    ast::{self, punctuated::Punctuated, Ast},
6    node::Node,
7    tokenizer::{Symbol, TokenType},
8    visitors::Visitor,
9};
10
11pub struct UnbalancedAssignmentsLint;
12
13impl Lint for UnbalancedAssignmentsLint {
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(UnbalancedAssignmentsLint)
22    }
23
24    fn pass(&self, ast: &Ast, _: &Context, _: &AstContext) -> Vec<Diagnostic> {
25        let mut visitor = UnbalancedAssignmentsVisitor {
26            assignments: Vec::new(),
27        };
28
29        visitor.visit_ast(ast);
30
31        visitor
32            .assignments
33            .drain(..)
34            .map(|assignment| {
35                if assignment.more {
36                    Diagnostic::new(
37                        "unbalanced_assignments",
38                        "too many values on the right side of the assignment".to_owned(),
39                        Label::new(assignment.range),
40                    )
41                } else {
42                    let secondary_labels = match assignment.first_call {
43                        Some(range) => vec![Label::new_with_message(
44                            range,
45                            "help: if this function returns more than one value, \
46                             the only first return value is actually used"
47                                .to_owned(),
48                        )],
49                        None => Vec::new(),
50                    };
51
52                    Diagnostic::new_complete(
53                        "unbalanced_assignments",
54                        "values on right side don't match up to the left side of the assignment"
55                            .to_owned(),
56                        Label::new(assignment.range),
57                        Vec::new(),
58                        secondary_labels,
59                    )
60                }
61            })
62            .collect()
63    }
64}
65
66struct UnbalancedAssignmentsVisitor {
67    assignments: Vec<UnbalancedAssignment>,
68}
69
70fn expression_is_call(expression: &ast::Expression) -> bool {
71    match expression {
72        ast::Expression::Parentheses { expression, .. } => expression_is_call(expression),
73        ast::Expression::FunctionCall(_) => true,
74
75        _ => false,
76    }
77}
78
79fn expression_is_nil(expression: &ast::Expression) -> bool {
80    match expression {
81        ast::Expression::Parentheses { expression, .. } => expression_is_call(expression),
82        ast::Expression::Symbol(symbol) => {
83            *symbol.token_type()
84                == TokenType::Symbol {
85                    symbol: Symbol::Nil,
86                }
87        }
88
89        _ => false,
90    }
91}
92
93fn expression_is_ellipsis(expression: &ast::Expression) -> bool {
94    if let ast::Expression::Symbol(symbol) = expression {
95        return *symbol.token_type()
96            == TokenType::Symbol {
97                symbol: Symbol::Ellipsis,
98            };
99    }
100
101    false
102}
103
104fn range<N: Node>(node: N) -> (u32, u32) {
105    let (start, end) = node.range().unwrap();
106    (start.bytes() as u32, end.bytes() as u32)
107}
108
109impl UnbalancedAssignmentsVisitor {
110    fn lint_assignment(&mut self, lhs: usize, rhs: &Punctuated<ast::Expression>) {
111        if rhs.is_empty() {
112            return;
113        }
114
115        let last_rhs = rhs.last().unwrap().value();
116
117        if rhs.len() > lhs {
118            self.assignments.push(UnbalancedAssignment {
119                more: true,
120                range: (
121                    // TODO: Implement Index and get() on Punctuated
122                    rhs.iter()
123                        .nth(lhs)
124                        .unwrap()
125                        .start_position()
126                        .unwrap()
127                        .bytes() as u32,
128                    last_rhs.end_position().unwrap().bytes() as u32,
129                ),
130                ..UnbalancedAssignment::default()
131            });
132        } else if rhs.len() < lhs
133            && !expression_is_ellipsis(last_rhs)
134            && !expression_is_call(last_rhs)
135            && !expression_is_nil(last_rhs)
136        {
137            self.assignments.push(UnbalancedAssignment {
138                first_call: rhs.iter().find(|e| expression_is_call(e)).map(range),
139                range: range(rhs),
140                ..UnbalancedAssignment::default()
141            });
142        }
143    }
144}
145
146impl Visitor for UnbalancedAssignmentsVisitor {
147    fn visit_assignment(&mut self, assignment: &ast::Assignment) {
148        self.lint_assignment(assignment.variables().len(), assignment.expressions());
149    }
150
151    fn visit_local_assignment(&mut self, assignment: &ast::LocalAssignment) {
152        self.lint_assignment(assignment.names().len(), assignment.expressions());
153    }
154}
155
156#[derive(Clone, Copy, Default)]
157struct UnbalancedAssignment {
158    first_call: Option<(u32, u32)>,
159    more: bool,
160    range: (u32, u32),
161}
162
163#[cfg(test)]
164mod tests {
165    use super::{super::test_util::test_lint, *};
166
167    #[test]
168    fn test_unbalanced_assignments() {
169        test_lint(
170            UnbalancedAssignmentsLint::new(()).unwrap(),
171            "unbalanced_assignments",
172            "unbalanced_assignments",
173        );
174    }
175}