artifact_app/cmd/
check.rs

1/* Copyright (c) 2017 Garrett Berg, vitiral@gmail.com
2 *
3 * Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4 * http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5 * http://opensource.org/licenses/MIT>, at your option. This file may not be
6 * copied, modified, or distributed except according to those terms.
7 */
8use dev_prefix::*;
9use types::*;
10use cmd::types::*;
11use utils;
12
13pub fn get_subcommand<'a, 'b>() -> App<'a, 'b> {
14    SubCommand::with_name("check")
15        .about("Check for any errors in the project")
16        .settings(&SUBCMD_SETTINGS)
17}
18
19// Helper functions
20fn paint_it<W: Write>(w: &mut W, msg: &str, cmd: &Cmd) {
21    if cmd.color {
22        write!(w, "{}", Red.paint(msg)).unwrap();
23    } else {
24        write!(w, "{}", msg).unwrap();
25    }
26}
27fn paint_it_bold<W: Write>(w: &mut W, msg: &str, cmd: &Cmd) {
28    if cmd.color {
29        write!(w, "{}", Red.bold().paint(msg)).unwrap();
30    } else {
31        write!(w, "{}", msg).unwrap();
32    }
33}
34
35fn display_invalid_partof<W: Write>(w: &mut W, cwd: &Path, project: &Project, cmd: &Cmd) -> u64 {
36    let mut error: u64 = 0;
37
38    // display invalid partof names and locations
39    let mut invalid_partof = Names::new();
40
41    // display artifacts with invalid partof names
42    let mut displayed_header = false;
43    for (name, artifact) in &project.artifacts {
44        invalid_partof.clear();
45        for p in &artifact.partof {
46            if !project.artifacts.contains_key(p) {
47                invalid_partof.insert(p.clone());
48            }
49        }
50        if !invalid_partof.is_empty() {
51            error += 1;
52            let mut msg = String::new();
53            if !displayed_header {
54                displayed_header = true;
55                paint_it_bold(w, "\nFound partof names that do not exist:\n", cmd);
56            }
57            write!(
58                msg,
59                "- {} [{}]: {:?}\n",
60                name,
61                utils::relative_path(&artifact.def, cwd).display(),
62                invalid_partof
63            ).unwrap();
64            paint_it(w, &msg, cmd);
65        }
66    }
67
68    error
69}
70
71fn display_unresolvable<W: Write>(w: &mut W, project: &Project, cmd: &Cmd) -> u64 {
72    let mut error: u64 = 0;
73
74    // display unresolvable partof names
75    let unresolved: Vec<(NameRc, &Artifact)> = Vec::from_iter(
76        project
77            .artifacts
78            .iter()
79            .filter(|a| a.1.completed < 0. || a.1.tested < 0.)
80            .map(|n| (n.0.clone(), n.1)),
81    );
82    let unknown_names: HashSet<NameRc> = HashSet::from_iter(unresolved.iter().map(|u| u.0.clone()));
83
84    if !unresolved.is_empty() {
85        error += 1;
86        let mut unresolved_partof: HashMap<NameRc, HashSet<NameRc>> = HashMap::new();
87        for &(ref name, artifact) in &unresolved {
88            let partof: HashSet<_> = artifact
89                .partof
90                .iter()
91                .filter(|n| {
92                    !project.artifacts.contains_key(n.as_ref()) ||
93                        unknown_names.contains(n.as_ref())
94                })
95                .cloned()
96                .collect();
97            unresolved_partof.insert(name.clone(), partof);
98        }
99
100        // reduce unresolved partof to only items that have at least one value
101        let mut resolved_names: HashSet<NameRc> = HashSet::new();
102        let mut remove_names = Vec::new();
103        let mut just_resolved = Vec::new();
104        loop {
105            just_resolved.clear();
106            let mut did_something = false;
107            for (name, partof) in &mut unresolved_partof {
108                // find names in partof that have no unresolved partofs and remove them
109                remove_names.clear();
110                for p in partof.iter() {
111                    if resolved_names.contains(p) {
112                        remove_names.push(p.clone());
113                    }
114                }
115                if !remove_names.is_empty() {
116                    did_something = true;
117                }
118                for p in &remove_names {
119                    partof.remove(p);
120                }
121
122                // if this artifact has no unresolved partofs, then it is considered resolved
123                if partof.is_empty() {
124                    resolved_names.insert(name.clone());
125                    just_resolved.push(name.clone());
126                }
127            }
128            if !just_resolved.is_empty() {
129                did_something = true;
130            }
131            for r in &just_resolved {
132                unresolved_partof.remove(r);
133            }
134            if !did_something {
135                break;
136            }
137        }
138        paint_it_bold(
139            w,
140            "\nArtifacts partof contains at least one recursive reference:\n",
141            cmd,
142        );
143        let mut unresolved_partof: Vec<_> = unresolved_partof
144            .drain()
145            .map(|mut v| (v.0, v.1.drain().collect::<Vec<_>>()))
146            .collect();
147        unresolved_partof.sort_by(|a, b| a.0.cmp(&b.0));
148        for (name, partof) in unresolved_partof.drain(0..) {
149            let mut msg = String::new();
150            write!(msg, "- {:<30}: {:?}\n", name.to_string(), partof).unwrap();
151            write!(w, "{}", msg).unwrap();
152        }
153    }
154
155    error
156}
157
158fn display_invalid_locs<W: Write>(w: &mut W, cwd: &Path, project: &Project, cmd: &Cmd) -> u64 {
159    let mut error: u64 = 0;
160
161    // display invalid locations
162    if !project.dne_locs.is_empty() {
163        error += 1;
164        // reorganize them by file
165        let mut invalid_locs: HashMap<PathBuf, Vec<(Name, Loc)>> = HashMap::new();
166        for (name, loc) in &project.dne_locs {
167            if !invalid_locs.contains_key(&loc.path) {
168                invalid_locs.insert(loc.path.clone(), Vec::new());
169            }
170            invalid_locs
171                .get_mut(&loc.path)
172                .unwrap()
173                .push((name.clone(), loc.clone()));
174        }
175        let header = "\nFound implementation links in the code that do not exist:\n";
176        paint_it_bold(w, header, cmd);
177        let mut invalid_locs: Vec<(PathBuf, Vec<(Name, Loc)>)> =
178            Vec::from_iter(invalid_locs.drain());
179        invalid_locs.sort_by(|a, b| a.0.cmp(&b.0));
180        for (path, mut locs) in invalid_locs.drain(0..) {
181            // sort by where they appear in the file
182            let mut pathstr = String::new();
183            write!(
184                pathstr,
185                "- {}:\n",
186                utils::relative_path(&path, cwd).display()
187            ).unwrap();
188            paint_it(w, &pathstr, cmd);
189            locs.sort_by(|a, b| a.1.line.cmp(&b.1.line));
190            for (name, loc) in locs {
191                let mut loc_str = String::new();
192                write!(loc_str, "  - [{}]", loc.line).unwrap();
193                paint_it(w, &loc_str, cmd);
194                write!(w, " {}\n", name).unwrap();
195            }
196        }
197    }
198
199    error
200}
201
202
203fn display_hanging_artifacts<W: Write>(w: &mut W, cwd: &Path, project: &Project, cmd: &Cmd) -> u64 {
204    let mut error: u64 = 0;
205
206    // find hanging artifacts
207    let mut hanging: Vec<(NameRc, &Path)> = Vec::new();
208    for (name, artifact) in &project.artifacts {
209        // hanging artifacts are defined as artifacts who are:
210        // - not a REQ (requirements are never hanging)
211        // - isn't a partof another artifact
212        if name.ty != Type::REQ && artifact.partof.is_empty() {
213            hanging.push((name.clone(), &artifact.def));
214        }
215    }
216    hanging.sort_by(|a, b| a.1.cmp(b.1));
217    if !hanging.is_empty() {
218        error += 1;
219        let msg = "\nHanging artifacts found (top-level but not partof a higher type):\n";
220        paint_it_bold(w, msg, cmd);
221        for (h, p) in hanging {
222            let mut msg = String::new();
223            write!(
224                msg,
225                "- {:<30}: {}\n",
226                utils::relative_path(p, cwd).display(),
227                h
228            ).unwrap();
229            write!(w, "{}", msg).unwrap();
230        }
231    }
232
233    error
234}
235
236fn display_hanging_references<W: Write>(
237    w: &mut W,
238    cwd: &Path,
239    project: &Project,
240    cmd: &Cmd,
241) -> u64 {
242    let mut error: u64 = 0;
243
244    let regexp = Regex::new(&format!(r"(?i)\[\[({})\]\]", NAME_VALID_STR)).expect("tested regexp");
245    let mut hanging: HashMap<NameRc, Vec<Name>> = HashMap::new();
246
247    for (name, artifact) in &project.artifacts {
248        let mut found = vec![];
249        for cap in regexp.captures_iter(&artifact.text) {
250            let raw = cap.get(1).expect("regexp definition");
251            let tname = Name::from_str(raw.as_str()).expect("regexp validatd");
252            if !project.artifacts.contains_key(&tname) {
253                error += 1;
254                found.push(tname);
255            }
256        }
257        if !found.is_empty() {
258            hanging.insert(name.clone(), found);
259        }
260    }
261
262    if !hanging.is_empty() {
263        paint_it_bold(
264            w,
265            "\nArtifacts text contains invalid [[ART-name]] references:\n",
266            cmd,
267        );
268        let mut hanging: Vec<_> = hanging.drain().collect();
269        hanging.sort();
270        for &(ref name, ref found) in &hanging {
271            let artifact = project.artifacts.get(name).expect("inserted from");
272            paint_it(
273                w,
274                &format!(
275                    "- {} ({}):\n",
276                    name,
277                    utils::relative_path(&artifact.def, cwd).display()
278                ),
279                cmd,
280            );
281            for f in found {
282                write!(w, "  - {}", f).unwrap();
283            }
284        }
285    }
286    error
287}
288
289pub struct Cmd {
290    pub color: bool,
291}
292
293
294pub fn display_check<W: Write>(w: &mut W, cwd: &Path, project: &Project, cmd: &Cmd) -> u64 {
295    let mut error: u64 = 0;
296    error += display_invalid_partof(w, cwd, project, cmd);
297    error += display_unresolvable(w, project, cmd);
298    error += display_invalid_locs(w, cwd, project, cmd);
299    error += display_hanging_artifacts(w, cwd, project, cmd);
300    error += display_hanging_references(w, cwd, project, cmd);
301    error
302}
303
304/// #SPC-cmd-check
305pub fn run_cmd<W: Write>(w: &mut W, cwd: &Path, project: &Project, cmd: &Cmd) -> Result<u8> {
306    let error = display_check(w, cwd, project, cmd);
307    if error == 0 {
308        let mut msg = String::new();
309        write!(msg, "art check: no errors found in {}\n", cwd.display()).unwrap();
310        if cmd.color {
311            write!(w, "{}", Green.paint(msg)).unwrap();
312        } else {
313            write!(w, "{}", msg).unwrap();
314        }
315    } else {
316        write!(w, "\n").unwrap();
317    }
318    if error != 0 {
319        Err(
320            ErrorKind::CmdError("errors found during ls, see logs".to_string()).into(),
321        )
322    } else {
323        Ok(0)
324    }
325}