1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
use crate::cop;
use crate::source;
use crate::types;
use std::collections::HashMap;
use std::sync::Mutex;

static COP_NAME: &str = "Bundler/DuplicatedGem";
static MSG: &str =
    "Gem `%gem_name%` requirements already given on line %line_number% of the Gemfile.";

lazy_static! {
    static ref GEMS: Mutex<HashMap<String, usize>> = Mutex::new(HashMap::new());
}

pub fn init() {
    cop::register(COP_NAME);
    cop::register_node_handler("send", COP_NAME, on_send);
}

pub fn on_send(node: &types::Node, file: &source::File) {
    let mut gems = GEMS.lock().unwrap();

    if let types::Node::Send(node) = node {
        if node.method_name == "gem" {
            if node.args.len() >= 1 {
                if let types::Node::Str(gem) = &node.args[0] {
                    let gem_name = gem.value.to_string().unwrap();

                    if let Some(line) = gems.get(&gem_name) {
                        let line = line + 1;

                        file.add_offense(
                            COP_NAME,
                            gem.expression_l,
                            MSG.replace("%gem_name%", &gem_name)
                                .replace("%line_number%", &line.to_string()),
                        );
                    } else {
                        let (line, _) = file
                            .parser_result
                            .input
                            .line_col_for_pos(gem.expression_l.begin)
                            .unwrap();

                        gems.insert(gem_name, line); // add line
                    }
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn it_detects_offenses() {
        crate::expect_offense!(
            "
            gem 'rails'
            gem 'rails'
        "
        );
    }
}