include_graph/dependencies/
configfile.rs

1use crate::dependencies::{
2    compiledb::parse_compile_database,
3    cparse::{all_sources_and_includes, extract_includes, SourceWithIncludes},
4    gn::load_gn_targets,
5    graph::GraphBuilder,
6    path_mapper::{PathMapper, PathMapping},
7};
8use color_eyre::Result;
9use color_eyre::{eyre::WrapErr, Report};
10use nom::{
11    branch::alt,
12    bytes::complete::{is_not, tag_no_case},
13    character::complete::{char as parsed_char, multispace1},
14    combinator::{opt, value},
15    multi::{many0, many1, separated_list0},
16    sequence::{pair, separated_pair, tuple},
17    IResult, Parser,
18};
19use nom_supreme::ParserExt;
20
21use std::{
22    collections::{HashMap, HashSet},
23    path::PathBuf,
24};
25
26use tracing::{debug, error, info, warn};
27
28use super::{error::Error, graph::Graph};
29
30/// Defines an instruction regarding name mapping
31#[derive(Debug, PartialEq, Clone)]
32pub enum MapInstruction {
33    DisplayMap { from: String, to: String },
34    Keep(String),
35    Drop(String),
36}
37
38#[derive(Debug, Clone, PartialEq)]
39pub enum GroupInstruction {
40    GroupSourceHeader,
41    GroupFromGn {
42        gn_root: String,
43        target: String,
44        source_root: String,
45        ignore_targets: HashSet<String>,
46    },
47    ManualGroup {
48        name: String,
49        color: Option<String>,
50        items: Vec<String>,
51    },
52}
53
54#[derive(Debug, PartialEq, Clone)]
55pub struct ZoomItem {
56    name: String,
57    focused: bool,
58}
59
60#[derive(Debug, PartialEq, Clone)]
61pub enum GroupEdgeEnd {
62    From(String),
63    To(String),
64}
65
66#[derive(Debug, PartialEq, Clone)]
67pub enum EdgeColor {
68    Regular(String),
69    Bold(String),
70}
71
72impl EdgeColor {
73    pub fn color_name(&self) -> &str {
74        match self {
75            EdgeColor::Regular(c) => c,
76            EdgeColor::Bold(c) => c,
77        }
78    }
79
80    pub fn is_bold(&self) -> bool {
81        match self {
82            EdgeColor::Regular(_) => false,
83            EdgeColor::Bold(_) => true,
84        }
85    }
86}
87
88#[derive(Debug, PartialEq, Clone)]
89pub struct ColorInstruction {
90    end: GroupEdgeEnd,
91    color: EdgeColor,
92}
93
94/// How a config file looks like
95#[derive(Debug, PartialEq, Clone)]
96enum InputCommand {
97    LoadCompileDb {
98        path: String,
99        load_include_directories: bool,
100        load_sources: bool,
101    },
102    IncludeDirectory(String),
103    Glob(String),
104}
105
106#[derive(Debug, PartialEq, Clone)]
107struct VariableAssignment {
108    name: String,
109    value: String,
110}
111
112#[derive(Debug, PartialEq, Default, Clone)]
113struct GraphInstructions {
114    map_instructions: Vec<MapInstruction>,
115    group_instructions: Vec<GroupInstruction>,
116    color_instructions: Vec<ColorInstruction>,
117    zoom_items: Vec<ZoomItem>,
118}
119
120/// Defines a full configuration file, with components
121/// resolved as much as possible
122#[derive(Debug, PartialEq, Clone, Default)]
123struct ConfigurationFile {
124    /// Fully resolved variables
125    variable_map: HashMap<String, String>,
126
127    /// What inputs are to be processed
128    input_commands: Vec<InputCommand>,
129
130    /// Instructions to build a braph
131    graph: GraphInstructions,
132}
133
134fn expand_variable(value: &str, variable_map: &HashMap<String, String>) -> String {
135    // expand any occurences of "${name}"
136    let mut value = value.to_string();
137
138    loop {
139        let replacements = variable_map
140            .iter()
141            .map(|(k, v)| (format!("${{{}}}", k), v))
142            .filter(|(k, _v)| value.contains(k))
143            .collect::<Vec<_>>();
144
145        if replacements.is_empty() {
146            break;
147        }
148
149        for (k, v) in replacements {
150            value = value.replace(&k, v);
151        }
152    }
153
154    value
155}
156
157/// Something that changes by self-expanding variables
158///
159/// Variable expansion is replacing "${name}" with the content of the
160/// key `name` in the variable_map hashmap
161trait Expanded {
162    fn expanded_from(self, variable_map: &HashMap<String, String>) -> Self;
163}
164
165trait ResolveVariables<O> {
166    fn resolve_variables(self) -> O;
167}
168
169impl ResolveVariables<HashMap<String, String>> for Vec<VariableAssignment> {
170    fn resolve_variables(self) -> HashMap<String, String> {
171        let mut variable_map = HashMap::new();
172        for VariableAssignment { name, value } in self {
173            variable_map.insert(name, value.expanded_from(&variable_map));
174        }
175        variable_map
176    }
177}
178
179impl Expanded for ZoomItem {
180    fn expanded_from(self, variable_map: &HashMap<String, String>) -> Self {
181        Self {
182            name: self.name.expanded_from(variable_map),
183            ..self
184        }
185    }
186}
187
188impl Expanded for GroupEdgeEnd {
189    fn expanded_from(self, variable_map: &HashMap<String, String>) -> Self {
190        match self {
191            GroupEdgeEnd::From(v) => GroupEdgeEnd::From(v.expanded_from(variable_map)),
192            GroupEdgeEnd::To(v) => GroupEdgeEnd::To(v.expanded_from(variable_map)),
193        }
194    }
195}
196
197impl Expanded for EdgeColor {
198    fn expanded_from(self, variable_map: &HashMap<String, String>) -> Self {
199        match self {
200            EdgeColor::Regular(c) => EdgeColor::Regular(c.expanded_from(variable_map)),
201            EdgeColor::Bold(c) => EdgeColor::Bold(c.expanded_from(variable_map)),
202        }
203    }
204}
205
206impl Expanded for ColorInstruction {
207    fn expanded_from(self, variable_map: &HashMap<String, String>) -> Self {
208        Self {
209            end: self.end.expanded_from(variable_map),
210            color: self.color.expanded_from(variable_map),
211        }
212    }
213}
214
215impl Expanded for InputCommand {
216    fn expanded_from(self, variable_map: &HashMap<String, String>) -> Self {
217        match self {
218            InputCommand::LoadCompileDb {
219                path,
220                load_include_directories,
221                load_sources,
222            } => InputCommand::LoadCompileDb {
223                path: path.expanded_from(variable_map),
224                load_include_directories,
225                load_sources,
226            },
227            InputCommand::IncludeDirectory(p) => {
228                InputCommand::IncludeDirectory(p.expanded_from(variable_map))
229            }
230            InputCommand::Glob(p) => InputCommand::Glob(p.expanded_from(variable_map)),
231        }
232    }
233}
234
235#[derive(Debug, Default)]
236struct DependencyData {
237    includes: HashSet<PathBuf>,
238    files: Vec<SourceWithIncludes>,
239}
240
241/// Pretty-print dependency data.
242///
243/// Wrapped as a separate struct to support lazy formatting
244struct FullFileList<'a> {
245    dependencies: &'a DependencyData,
246}
247
248impl<'a> FullFileList<'a> {
249    pub fn new(dependencies: &'a DependencyData) -> Self {
250        Self { dependencies }
251    }
252}
253
254impl<'a> std::fmt::Display for FullFileList<'a> {
255    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        fmt.write_str("Processed files:\n")?;
257        for f in self.dependencies.files.iter() {
258            fmt.write_fmt(format_args!("  {:?}\n", f.path))?;
259        }
260        Ok(())
261    }
262}
263
264/// Parse an individual comment.
265///
266/// ```
267/// # use include_graph::dependencies::configfile::parse_comment;
268///
269/// assert_eq!(parse_comment("# foo"), Ok(("", " foo")));
270/// assert_eq!(parse_comment("# foo\ntest"), Ok(("\ntest", " foo")));
271/// assert_eq!(parse_comment("# foo\n#bar"), Ok(("\n#bar", " foo")));
272/// assert!(parse_comment("blah").is_err());
273/// assert!(parse_comment("blah # rest").is_err());
274///
275/// ```
276pub fn parse_comment(input: &str) -> IResult<&str, &str> {
277    pair(parsed_char('#'), opt(is_not("\n\r")))
278        .map(|(_, r)| r.unwrap_or_default())
279        .parse(input)
280}
281
282/// Parse whitespace until the first non-whitespace character
283///
284/// Whitespace is considered:
285///    - actual space
286///    - any comment(s)
287///
288/// ```
289/// # use include_graph::dependencies::configfile::parse_whitespace;
290///
291/// assert_eq!(parse_whitespace("  \n  foo"), Ok(("foo", ())));
292/// assert_eq!(parse_whitespace(" # test"), Ok(("", ())));
293/// assert_eq!(parse_whitespace("# rest"), Ok(("", ())));
294/// assert_eq!(parse_whitespace("  # test\n  \t# more\n  last\nthing"), Ok(("last\nthing", ())));
295///
296/// assert!(parse_whitespace("blah").is_err());
297///
298/// ```
299pub fn parse_whitespace(input: &str) -> IResult<&str, ()> {
300    value((), many1(alt((multispace1, parse_comment)))).parse(input)
301}
302
303fn parse_variable_name(input: &str) -> IResult<&str, &str> {
304    is_not("= \t\r\n{}[]()#").parse(input)
305}
306
307/// Parse things until the first whitespace character (comments are whitespace).
308/// Parsing until end of input is also acceptable (end of input is whitespace)
309///
310/// ```
311/// # use include_graph::dependencies::configfile::parse_until_whitespace;
312///
313/// assert_eq!(parse_until_whitespace("x  \n  foo"), Ok(("  \n  foo", "x")));
314/// assert_eq!(parse_until_whitespace("foo bar"), Ok((" bar", "foo")));
315/// assert_eq!(parse_until_whitespace("foo"), Ok(("", "foo")));
316/// assert_eq!(parse_until_whitespace("foo# comment"), Ok(("# comment", "foo")));
317/// assert_eq!(parse_until_whitespace("foo then a # comment"), Ok((" then a # comment", "foo")));
318/// assert!(parse_until_whitespace("\n  foo").is_err());
319///
320/// ```
321pub fn parse_until_whitespace(input: &str) -> IResult<&str, &str> {
322    is_not("#\n\r \t").parse(input)
323}
324
325fn parse_compiledb(input: &str) -> IResult<&str, InputCommand> {
326    #[derive(Clone, Copy, PartialEq)]
327    enum Type {
328        Includes,
329        Sources,
330    }
331    tuple((
332        parse_until_whitespace
333            .preceded_by(tuple((
334                opt(parse_whitespace),
335                tag_no_case("from"),
336                parse_whitespace,
337                tag_no_case("compiledb"),
338                parse_whitespace,
339            )))
340            .terminated(parse_whitespace),
341        separated_list0(
342            tuple((
343                opt(parse_whitespace),
344                tag_no_case(","),
345                opt(parse_whitespace),
346            )),
347            alt((
348                value(Type::Includes, tag_no_case("include_dirs")),
349                value(Type::Sources, tag_no_case("sources")),
350            )),
351        )
352        .preceded_by(tuple((tag_no_case("load"), opt(parse_whitespace))))
353        .terminated(opt(parse_whitespace)),
354    ))
355    .map(|(path, selections)| InputCommand::LoadCompileDb {
356        path: path.into(),
357        load_include_directories: selections.contains(&Type::Includes),
358        load_sources: selections.contains(&Type::Sources),
359    })
360    .parse(input)
361}
362
363fn parse_input_command(input: &str) -> IResult<&str, InputCommand> {
364    alt((
365        parse_compiledb,
366        parse_until_whitespace
367            .preceded_by(tuple((tag_no_case("glob"), parse_whitespace)))
368            .terminated(opt(parse_whitespace))
369            .map(|s| InputCommand::Glob(s.into())),
370        parse_until_whitespace
371            .preceded_by(tuple((tag_no_case("include_dir"), parse_whitespace)))
372            .terminated(opt(parse_whitespace))
373            .map(|s| InputCommand::IncludeDirectory(s.into())),
374    ))
375    .parse(input)
376}
377
378fn parse_input(input: &str) -> IResult<&str, Vec<InputCommand>> {
379    many0(parse_input_command)
380        .preceded_by(tuple((
381            tag_no_case("input"),
382            parse_whitespace,
383            tag_no_case("{"),
384            opt(parse_whitespace),
385        )))
386        .terminated(tuple((tag_no_case("}"), opt(parse_whitespace))))
387        .parse(input)
388}
389
390impl<T> Expanded for Vec<T>
391where
392    T: Expanded,
393{
394    fn expanded_from(self, variable_map: &HashMap<String, String>) -> Self {
395        self.into_iter()
396            .map(|v| v.expanded_from(variable_map))
397            .collect()
398    }
399}
400
401impl Expanded for String {
402    fn expanded_from(self, variable_map: &HashMap<String, String>) -> Self {
403        expand_variable(&self, variable_map)
404    }
405}
406
407impl Expanded for MapInstruction {
408    fn expanded_from(self, variable_map: &HashMap<String, String>) -> Self {
409        match self {
410            MapInstruction::DisplayMap { from, to } => MapInstruction::DisplayMap {
411                from: from.expanded_from(variable_map),
412                to: to.expanded_from(variable_map),
413            },
414            MapInstruction::Keep(v) => MapInstruction::Keep(v.expanded_from(variable_map)),
415            MapInstruction::Drop(v) => MapInstruction::Drop(v.expanded_from(variable_map)),
416        }
417    }
418}
419
420impl Expanded for GroupInstruction {
421    fn expanded_from(self, variable_map: &HashMap<String, String>) -> Self {
422        match self {
423            GroupInstruction::GroupSourceHeader => self,
424            GroupInstruction::GroupFromGn {
425                gn_root,
426                target,
427                source_root,
428                ignore_targets,
429            } => GroupInstruction::GroupFromGn {
430                gn_root: gn_root.expanded_from(variable_map),
431                target,
432                source_root: source_root.expanded_from(variable_map),
433                ignore_targets,
434            },
435            GroupInstruction::ManualGroup { name, color, items } => {
436                GroupInstruction::ManualGroup { name, color, items }
437            }
438        }
439    }
440}
441
442impl Expanded for GraphInstructions {
443    fn expanded_from(self, variable_map: &HashMap<String, String>) -> Self {
444        Self {
445            map_instructions: self.map_instructions.expanded_from(variable_map),
446            group_instructions: self.group_instructions.expanded_from(variable_map),
447            color_instructions: self.color_instructions.expanded_from(variable_map),
448            zoom_items: self.zoom_items.expanded_from(variable_map),
449        }
450    }
451}
452
453fn parse_map_instructions(input: &str) -> IResult<&str, Vec<MapInstruction>> {
454    many0(
455        alt((
456            separated_pair(
457                parse_until_whitespace,
458                tuple((parse_whitespace, tag_no_case("=>"), parse_whitespace)),
459                parse_until_whitespace,
460            )
461            .map(|(from, to)| MapInstruction::DisplayMap {
462                from: from.into(),
463                to: to.into(),
464            }),
465            parse_until_whitespace
466                .preceded_by(tuple((
467                    opt(parse_whitespace),
468                    tag_no_case("keep"),
469                    parse_whitespace,
470                )))
471                .map(|s| MapInstruction::Keep(s.into())),
472            parse_until_whitespace
473                .preceded_by(tuple((
474                    opt(parse_whitespace),
475                    tag_no_case("drop"),
476                    parse_whitespace,
477                )))
478                .map(|s| MapInstruction::Drop(s.into())),
479        ))
480        .terminated(parse_whitespace),
481    )
482    .preceded_by(tuple((
483        tag_no_case("map"),
484        parse_whitespace,
485        tag_no_case("{"),
486        parse_whitespace,
487    )))
488    .terminated(tuple((
489        opt(parse_whitespace),
490        tag_no_case("}"),
491        opt(parse_whitespace),
492    )))
493    .parse(input)
494}
495
496fn parse_gn_target(input: &str) -> IResult<&str, GroupInstruction> {
497    tuple((
498        parse_until_whitespace.preceded_by(tuple((
499            tag_no_case("gn"),
500            parse_whitespace,
501            tag_no_case("root"),
502            parse_whitespace,
503        ))),
504        parse_until_whitespace.preceded_by(tuple((
505            parse_whitespace,
506            tag_no_case("target"),
507            parse_whitespace,
508        ))),
509        parse_until_whitespace.preceded_by(tuple((
510            parse_whitespace,
511            tag_no_case("sources"),
512            parse_whitespace,
513        ))),
514        opt(parse_target_list
515            .preceded_by(tuple((
516                parse_whitespace,
517                tag_no_case("ignore"),
518                parse_whitespace,
519                tag_no_case("targets"),
520                opt(parse_whitespace),
521                tag_no_case("{"),
522            )))
523            .terminated(tuple((
524                opt(parse_whitespace),
525                tag_no_case("}"),
526                opt(parse_whitespace),
527            )))),
528    ))
529    .terminated(opt(parse_whitespace))
530    .map(
531        |(gn_root, target, source_root, ignore_targets)| GroupInstruction::GroupFromGn {
532            gn_root: gn_root.into(),
533            target: target.into(),
534            source_root: source_root.into(),
535            ignore_targets: ignore_targets
536                .map(|v| v.into_iter().map(|s| s.into()).collect())
537                .unwrap_or_default(),
538        },
539    )
540    .parse(input)
541}
542
543fn parse_manual_group(input: &str) -> IResult<&str, GroupInstruction> {
544    tuple((
545        tuple((
546            parse_until_whitespace,
547            opt(parse_until_whitespace.preceded_by(tuple((
548                parse_whitespace,
549                tag_no_case("color"),
550                opt(parse_whitespace),
551            )))),
552        ))
553        .preceded_by(tuple((
554            opt(parse_whitespace),
555            tag_no_case("manual"),
556            opt(parse_whitespace),
557        )))
558        .terminated(tuple((opt(parse_whitespace), tag_no_case("{")))),
559        many0(
560            is_not("\n\r \t#}")
561                .preceded_by(opt(parse_whitespace))
562                .map(String::from),
563        ),
564    ))
565    .map(|((name, color), items)| GroupInstruction::ManualGroup {
566        name: name.into(),
567        color: color.map(|c| c.into()),
568        items,
569    })
570    .terminated(tuple((
571        opt(parse_whitespace),
572        tag_no_case("}"),
573        opt(parse_whitespace),
574    )))
575    .parse(input)
576}
577
578fn parse_target_list(input: &str) -> IResult<&str, Vec<&str>> {
579    many0(is_not("\n\r \t#}").preceded_by(opt(parse_whitespace)))
580        .terminated(opt(parse_whitespace))
581        .parse(input)
582}
583
584fn parse_group_by_extension(input: &str) -> IResult<&str, GroupInstruction> {
585    // TODO: in the future consider if we should allow a "group these extensions"
586    //       instead of automatic
587    //
588    //       Automatic seems nice because it just strips extensions regardless of
589    //       content.
590    value(
591        GroupInstruction::GroupSourceHeader,
592        tag_no_case("group_source_header"),
593    )
594    .terminated(opt(parse_whitespace))
595    .parse(input)
596}
597
598fn parse_group(input: &str) -> IResult<&str, Vec<GroupInstruction>> {
599    many0(alt((
600        parse_group_by_extension,
601        parse_gn_target,
602        parse_manual_group,
603    )))
604    .preceded_by(tuple((
605        opt(parse_whitespace),
606        tag_no_case("group"),
607        opt(parse_whitespace),
608        tag_no_case("{"),
609        opt(parse_whitespace),
610    )))
611    .terminated(tuple((
612        opt(parse_whitespace),
613        tag_no_case("}"),
614        opt(parse_whitespace),
615    )))
616    .parse(input)
617}
618
619fn parse_color_instruction(input: &str) -> IResult<&str, ColorInstruction> {
620    #[derive(Copy, Clone, PartialEq)]
621    enum Direction {
622        From,
623        To,
624    }
625
626    tuple((
627        alt((
628            value(Direction::From, tag_no_case("from")),
629            value(Direction::To, tag_no_case("to")),
630        ))
631        .preceded_by(opt(parse_whitespace))
632        .terminated(parse_whitespace),
633        parse_until_whitespace.terminated(parse_whitespace),
634        opt(tag_no_case("bold").terminated(parse_whitespace)).map(|v| v.is_some()),
635        parse_until_whitespace,
636    ))
637    .map(|(direction, name, bold, color)| ColorInstruction {
638        end: match direction {
639            Direction::From => GroupEdgeEnd::From(name.into()),
640            Direction::To => GroupEdgeEnd::To(name.into()),
641        },
642        color: if bold {
643            EdgeColor::Bold(color.into())
644        } else {
645            EdgeColor::Regular(color.into())
646        },
647    })
648    .parse(input)
649}
650
651fn parse_color_instructions(input: &str) -> IResult<&str, Vec<ColorInstruction>> {
652    separated_list0(parse_whitespace, parse_color_instruction)
653        .preceded_by(tuple((
654            opt(parse_whitespace),
655            tag_no_case("color"),
656            parse_whitespace,
657            tag_no_case("edges"),
658            opt(parse_whitespace),
659            tag_no_case("{"),
660            opt(parse_whitespace),
661        )))
662        .terminated(tuple((
663            opt(parse_whitespace),
664            tag_no_case("}"),
665            opt(parse_whitespace),
666        )))
667        .parse(input)
668}
669
670fn parse_zoom(input: &str) -> IResult<&str, Vec<ZoomItem>> {
671    many0(
672        tuple((
673            opt(tag_no_case("focus:").terminated(parse_whitespace)),
674            is_not("\n\r \t#}").terminated(opt(parse_whitespace)),
675        ))
676        .map(|(focus, name)| ZoomItem {
677            name: name.into(),
678            focused: focus.is_some(),
679        }),
680    )
681    .preceded_by(tuple((
682        opt(parse_whitespace),
683        tag_no_case("zoom"),
684        opt(parse_whitespace),
685        tag_no_case("{"),
686        opt(parse_whitespace),
687    )))
688    .terminated(tuple((
689        opt(parse_whitespace),
690        tag_no_case("}"),
691        opt(parse_whitespace),
692    )))
693    .parse(input)
694}
695
696fn parse_graph(input: &str) -> IResult<&str, GraphInstructions> {
697    tuple((
698        parse_map_instructions,
699        parse_group,
700        opt(parse_color_instructions),
701        opt(parse_zoom),
702    ))
703    .preceded_by(tuple((
704        opt(parse_whitespace),
705        tag_no_case("graph"),
706        parse_whitespace,
707        tag_no_case("{"),
708        opt(parse_whitespace),
709    )))
710    .terminated(tuple((
711        opt(parse_whitespace),
712        tag_no_case("}"),
713        opt(parse_whitespace),
714    )))
715    .map(
716        |(map_instructions, group_instructions, color_instructions, zoom)| GraphInstructions {
717            map_instructions,
718            group_instructions,
719            color_instructions: color_instructions.unwrap_or_default(),
720            zoom_items: zoom.unwrap_or_default(),
721        },
722    )
723    .parse(input)
724}
725
726fn parse_variable_assignment(input: &str) -> IResult<&str, VariableAssignment> {
727    separated_pair(
728        parse_variable_name,
729        tag_no_case("=")
730            .preceded_by(opt(parse_whitespace))
731            .terminated(opt(parse_whitespace)),
732        parse_until_whitespace,
733    )
734    .map(|(name, value)| VariableAssignment {
735        name: name.into(),
736        value: value.into(),
737    })
738    .parse(input)
739}
740
741fn parse_variable_assignments(input: &str) -> IResult<&str, HashMap<String, String>> {
742    separated_list0(parse_whitespace, parse_variable_assignment)
743        .preceded_by(opt(parse_whitespace))
744        .terminated(opt(parse_whitespace))
745        .map(|v| v.resolve_variables())
746        .parse(input)
747}
748
749fn parse_config(input: &str) -> IResult<&str, ConfigurationFile> {
750    tuple((parse_variable_assignments, parse_input, parse_graph))
751        .map(|(variable_map, input_commands, graph)| ConfigurationFile {
752            input_commands: input_commands.expanded_from(&variable_map),
753            graph: graph.expanded_from(&variable_map),
754            variable_map,
755        })
756        .parse(input)
757}
758
759pub async fn build_graph(input: &str) -> Result<Graph, Report> {
760    let (input, config) = parse_config(input)
761        .map_err(|e| Error::ConfigParseError {
762            message: format!("Nom error: {:?}", e),
763        })
764        .wrap_err("Failed to parse with nom")?;
765
766    if !input.is_empty() {
767        return Err(Error::ConfigParseError {
768            message: format!("Not all input was consumed: {:?}", input),
769        }
770        .into());
771    }
772
773    debug!("Variables: {:#?}", config.variable_map);
774    debug!("Input:     {:#?}", config.input_commands);
775    debug!("Graph:     {:#?}", config.graph);
776
777    let mut dependency_data = DependencyData::default();
778
779    for i in config.input_commands {
780        match i {
781            InputCommand::LoadCompileDb {
782                path,
783                load_include_directories,
784                load_sources,
785            } => {
786                let entries = match parse_compile_database(&path).await {
787                    Ok(entries) => entries,
788                    Err(err) => {
789                        error!("Error parsing compile database {}: {:?}", path, err);
790                        continue;
791                    }
792                };
793                if entries.is_empty() {
794                    warn!(target: "compile-db", "No entries loaded from {}", path);
795                    warn!(target: "compile-db",
796                    "This may happen if directory/source paths are not correct (e.g. compilation on a different system)."
797                    );
798                    warn!(target: "compile-db",
799                        "Consider debugging by setting RUST_LOG=\"compile-db=debug\" to debug entry parsing."
800                    );
801                }
802                info!(target: "compile-db", "Loaded {} compile entries from {}", entries.len(), &path);
803                if load_include_directories {
804                    let compile_db_includes = entries
805                        .iter()
806                        .flat_map(|e| e.include_directories.clone())
807                        .collect::<HashSet<_>>();
808                    info!(target: "compile-db",
809                            "Include directories from {}: {:#?}", &path, compile_db_includes);
810
811                    dependency_data.includes.extend(compile_db_includes);
812                }
813                if load_sources {
814                    let includes_array = dependency_data
815                        .includes
816                        .clone()
817                        .into_iter()
818                        .collect::<Vec<_>>();
819                    for entry in entries {
820                        match extract_includes(&entry.file_path, &includes_array).await {
821                            Ok(includes) => {
822                                info!(target: "compile-db", "Loaded {:?} with includes {:#?}", &entry.file_path, includes);
823                                dependency_data.files.push(SourceWithIncludes {
824                                    path: entry.file_path,
825                                    includes,
826                                });
827                            }
828                            Err(e) => {
829                                error!(
830                                    "Includee extraction for {:?} failed: {:?}",
831                                    &entry.file_path, e
832                                );
833                            }
834                        };
835                    }
836                }
837            }
838            InputCommand::IncludeDirectory(path) => {
839                dependency_data.includes.insert(PathBuf::from(path));
840            }
841            InputCommand::Glob(g) => {
842                let glob = match glob::glob(&g) {
843                    Ok(value) => value,
844                    Err(e) => {
845                        error!("Glob error for {}: {:?}", g, e);
846                        continue;
847                    }
848                };
849                let includes_array = dependency_data
850                    .includes
851                    .clone()
852                    .into_iter()
853                    .collect::<Vec<_>>();
854                match all_sources_and_includes(glob, &includes_array).await {
855                    Ok(data) => {
856                        if data.is_empty() {
857                            error!("GLOB {:?} resulted in EMPTY file list!", g);
858                        }
859                        dependency_data.files.extend(data)
860                    }
861                    Err(e) => {
862                        error!("Include prodcessing for {} failed: {:?}", g, e);
863                        continue;
864                    }
865                }
866            }
867        }
868    }
869
870    // set up a path mapper
871    let mut mapper = PathMapper::default();
872    for i in config.graph.map_instructions.iter() {
873        if let MapInstruction::DisplayMap { from, to } = i {
874            mapper.add_mapping(PathMapping {
875                from: PathBuf::from(from),
876                to: to.clone(),
877            });
878        }
879    }
880    let keep = config
881        .graph
882        .map_instructions
883        .iter()
884        .filter_map(|i| match i {
885            MapInstruction::Keep(v) => Some(v),
886            _ => None,
887        })
888        .collect::<HashSet<_>>();
889
890    let drop = config
891        .graph
892        .map_instructions
893        .iter()
894        .filter_map(|i| match i {
895            MapInstruction::Drop(v) => Some(v),
896            _ => None,
897        })
898        .collect::<HashSet<_>>();
899
900    info!(target: "full-file-list", "Procesed files: {}", FullFileList::new(&dependency_data));
901
902    // Dependency data is prunned based on instructions
903    let mut g = GraphBuilder::new(
904        dependency_data
905            .files
906            .iter()
907            .flat_map(|f| f.includes.iter().chain(std::iter::once(&f.path)))
908            .filter_map(|path| {
909                mapper.try_map(path).map(|to| PathMapping {
910                    from: path.clone(),
911                    to,
912                })
913            })
914            .filter(|m| keep.iter().any(|prefix| m.to.starts_with(*prefix)))
915            .filter(|m| drop.iter().all(|prefix| !m.to.starts_with(*prefix))),
916    );
917
918    // define all the groups
919    for group_instruction in config.graph.group_instructions {
920        match group_instruction {
921            GroupInstruction::GroupSourceHeader => {
922                g.group_extensions(&["h", "cpp", "hpp", "c", "cxx"]);
923            }
924            GroupInstruction::GroupFromGn {
925                gn_root,
926                target,
927                source_root,
928                ignore_targets,
929            } => match load_gn_targets(
930                &PathBuf::from(gn_root),
931                &PathBuf::from(source_root),
932                &target,
933            )
934            .await
935            {
936                Ok(targets) => g.add_groups_from_gn(targets, ignore_targets),
937                Err(e) => error!("Failed to load GN targets: {:?}", e),
938            },
939            GroupInstruction::ManualGroup { name, color, items } => {
940                // items here are mapped, so we have to invert the map to get
941                // the actual name...
942                g.define_group(
943                    &name,
944                    color.as_deref().unwrap_or("orange"),
945                    items.into_iter().filter_map(|n| mapper.try_invert(&n)),
946                );
947            }
948        }
949    }
950
951    // mark what is zoomed in ...
952    for item in config.graph.zoom_items {
953        g.zoom_in(&item.name, item.focused)
954    }
955
956    for dep in dependency_data.files {
957        if !g.known_path(&dep.path) {
958            continue;
959        }
960        for dest in dep.includes {
961            if !g.known_path(&dest) {
962                continue;
963            }
964            g.add_link(&dep.path, &dest);
965        }
966    }
967
968    for i in config.graph.color_instructions {
969        match i.end {
970            GroupEdgeEnd::From(name) => {
971                g.color_from(&name, i.color.color_name(), i.color.is_bold())
972            }
973            GroupEdgeEnd::To(name) => g.color_to(&name, i.color.color_name(), i.color.is_bold()),
974        }
975    }
976
977    debug!("Final builder: {:#?}", g);
978
979    Ok(g.build())
980}
981
982#[cfg(test)]
983mod tests {
984    use super::*;
985
986    #[test]
987    fn test_comment_parsing() {
988        assert_eq!(parse_comment("#abc\r\nhello"), Ok(("\r\nhello", "abc")));
989        assert!(parse_comment("not a comment").is_err());
990        assert!(parse_comment("comment later # like here").is_err());
991    }
992
993    #[test]
994    fn test_gn_target() {
995        assert_eq!(
996            parse_gn_target("gn root test1 target //my/target/* sources srcs1"),
997            Ok((
998                "",
999                GroupInstruction::GroupFromGn {
1000                    gn_root: "test1".into(),
1001                    target: "//my/target/*".into(),
1002                    source_root: "srcs1".into(),
1003                    ignore_targets: HashSet::new(),
1004                },
1005            ))
1006        );
1007
1008        assert_eq!(
1009            parse_gn_target("gn root test1 target //my/target/* sources srcs1 ignore targets {}"),
1010            Ok((
1011                "",
1012                GroupInstruction::GroupFromGn {
1013                    gn_root: "test1".into(),
1014                    target: "//my/target/*".into(),
1015                    source_root: "srcs1".into(),
1016                    ignore_targets: HashSet::new(),
1017                },
1018            ))
1019        );
1020
1021        assert_eq!(
1022            parse_gn_target(
1023                "gn root test1 target //my/target/* sources srcs1 ignore targets{
1024            }"
1025            ),
1026            Ok((
1027                "",
1028                GroupInstruction::GroupFromGn {
1029                    gn_root: "test1".into(),
1030                    target: "//my/target/*".into(),
1031                    source_root: "srcs1".into(),
1032                    ignore_targets: HashSet::new(),
1033                },
1034            ))
1035        );
1036
1037        assert_eq!(
1038            parse_gn_target(
1039                "gn root test1 target //my/target/* sources srcs1 ignore targets{
1040                a b
1041                c
1042                d
1043            }"
1044            ),
1045            Ok((
1046                "",
1047                GroupInstruction::GroupFromGn {
1048                    gn_root: "test1".into(),
1049                    target: "//my/target/*".into(),
1050                    source_root: "srcs1".into(),
1051                    ignore_targets: vec!["a", "b", "c", "d"]
1052                        .into_iter()
1053                        .map(String::from)
1054                        .collect(),
1055                },
1056            ))
1057        );
1058    }
1059
1060    #[test]
1061    fn test_manual_group() {
1062        assert_eq!(
1063            parse_manual_group(
1064                "
1065            manual some/name::special {
1066                file1
1067                file2
1068                another/file::test
1069            }
1070            "
1071            ),
1072            Ok((
1073                "",
1074                GroupInstruction::ManualGroup {
1075                    name: "some/name::special".into(),
1076                    color: None,
1077                    items: vec!["file1".into(), "file2".into(), "another/file::test".into(),]
1078                }
1079            ))
1080        );
1081
1082        assert_eq!(
1083            parse_manual_group(
1084                "
1085            manual some/name::special color red {
1086                file1
1087                file2
1088                another/file::test
1089            }
1090            "
1091            ),
1092            Ok((
1093                "",
1094                GroupInstruction::ManualGroup {
1095                    name: "some/name::special".into(),
1096                    color: Some("red".into()),
1097                    items: vec!["file1".into(), "file2".into(), "another/file::test".into(),]
1098                }
1099            ))
1100        );
1101    }
1102
1103    #[test]
1104    fn test_gn_instruction() {
1105        let mut variable_map = HashMap::new();
1106        variable_map.insert("Foo".into(), "Bar".into());
1107
1108        assert_eq!(
1109            parse_graph(
1110                "
1111        graph {
1112              map {
1113              }
1114   
1115              group {
1116                gn root test1 target //my/target/* sources srcs1
1117                gn root test/${Foo}/blah target //* sources ${Foo} ignore targets {
1118                    //ignore1
1119                    //ignore:other
1120                }
1121              }
1122        }
1123        ",
1124            )
1125            .map(|(r, g)| { (r, g.expanded_from(&variable_map)) }),
1126            Ok((
1127                "",
1128                GraphInstructions {
1129                    map_instructions: Vec::default(),
1130                    group_instructions: vec![
1131                        GroupInstruction::GroupFromGn {
1132                            gn_root: "test1".into(),
1133                            target: "//my/target/*".into(),
1134                            source_root: "srcs1".into(),
1135                            ignore_targets: HashSet::new(),
1136                        },
1137                        GroupInstruction::GroupFromGn {
1138                            gn_root: "test/Bar/blah".into(),
1139                            target: "//*".into(),
1140                            source_root: "Bar".into(),
1141                            ignore_targets: {
1142                                let mut h = HashSet::new();
1143                                h.insert("//ignore1".into());
1144                                h.insert("//ignore:other".into());
1145                                h
1146                            }
1147                        },
1148                    ],
1149                    ..Default::default()
1150                }
1151            ))
1152        );
1153    }
1154
1155    #[test]
1156    fn test_color_instruction_parsing() {
1157        assert_eq!(
1158            parse_color_instruction("from source color"),
1159            Ok((
1160                "",
1161                ColorInstruction {
1162                    end: GroupEdgeEnd::From("source".into()),
1163                    color: EdgeColor::Regular("color".into())
1164                }
1165            ))
1166        );
1167
1168        assert_eq!(
1169            parse_color_instruction("to destination color"),
1170            Ok((
1171                "",
1172                ColorInstruction {
1173                    end: GroupEdgeEnd::To("destination".into()),
1174                    color: EdgeColor::Regular("color".into())
1175                }
1176            ))
1177        );
1178
1179        assert_eq!(
1180            parse_color_instruction("From x y"),
1181            Ok((
1182                "",
1183                ColorInstruction {
1184                    end: GroupEdgeEnd::From("x".into()),
1185                    color: EdgeColor::Regular("y".into())
1186                }
1187            ))
1188        );
1189
1190        assert_eq!(
1191            parse_color_instruction("#comment\n  TO a bold b"),
1192            Ok((
1193                "",
1194                ColorInstruction {
1195                    end: GroupEdgeEnd::To("a".into()),
1196                    color: EdgeColor::Bold("b".into())
1197                }
1198            ))
1199        );
1200    }
1201
1202    #[test]
1203    fn test_color_instructions_parsing() {
1204        assert_eq!(
1205            parse_color_instructions("color edges {}"),
1206            Ok(("", Vec::default()))
1207        );
1208        assert_eq!(
1209            parse_color_instructions(" #comment\ncolor edges {  \n  }\n#more comments\n   \n"),
1210            Ok(("", Vec::default()))
1211        );
1212
1213        assert_eq!(
1214            parse_color_instructions(
1215                "
1216         #comment
1217         color edges {
1218            from x y
1219            to q r
1220            from a bold b
1221         }"
1222            ),
1223            Ok((
1224                "",
1225                vec![
1226                    ColorInstruction {
1227                        end: GroupEdgeEnd::From("x".into()),
1228                        color: EdgeColor::Regular("y".into()),
1229                    },
1230                    ColorInstruction {
1231                        end: GroupEdgeEnd::To("q".into()),
1232                        color: EdgeColor::Regular("r".into()),
1233                    },
1234                    ColorInstruction {
1235                        end: GroupEdgeEnd::From("a".into()),
1236                        color: EdgeColor::Bold("b".into()),
1237                    },
1238                ]
1239            ))
1240        );
1241
1242        assert_eq!(
1243            parse_zoom(
1244                "
1245         #comment
1246         zoom{
1247            normal
1248            focus: thisone
1249            not this
1250         }"
1251            ),
1252            Ok((
1253                "",
1254                vec![
1255                    ZoomItem {
1256                        name: "normal".to_string(),
1257                        focused: false
1258                    },
1259                    ZoomItem {
1260                        name: "thisone".to_string(),
1261                        focused: true
1262                    },
1263                    ZoomItem {
1264                        name: "not".to_string(),
1265                        focused: false
1266                    },
1267                    ZoomItem {
1268                        name: "this".to_string(),
1269                        focused: false
1270                    },
1271                ]
1272            ))
1273        );
1274
1275        assert!(parse_zoom("blah").is_err());
1276    }
1277
1278    #[test]
1279    fn test_zoom_parsing() {
1280        assert_eq!(parse_zoom("zoom{}"), Ok(("", Vec::default())));
1281        assert_eq!(
1282            parse_zoom(" #comment\nzoom {  \n  }\n#more comments\n   \n"),
1283            Ok(("", Vec::default()))
1284        );
1285
1286        assert_eq!(
1287            parse_zoom(
1288                "
1289         #comment
1290         zoom{
1291            this
1292            is some #notice that whitespace matters and NOT newlines
1293            test
1294         }"
1295            ),
1296            Ok((
1297                "",
1298                vec![
1299                    ZoomItem {
1300                        name: "this".to_string(),
1301                        focused: false
1302                    },
1303                    ZoomItem {
1304                        name: "is".to_string(),
1305                        focused: false
1306                    },
1307                    ZoomItem {
1308                        name: "some".to_string(),
1309                        focused: false
1310                    },
1311                    ZoomItem {
1312                        name: "test".to_string(),
1313                        focused: false
1314                    },
1315                ]
1316            ))
1317        );
1318
1319        assert_eq!(
1320            parse_zoom(
1321                "
1322         #comment
1323         zoom{
1324            normal
1325            focus: thisone
1326            not this
1327         }"
1328            ),
1329            Ok((
1330                "",
1331                vec![
1332                    ZoomItem {
1333                        name: "normal".to_string(),
1334                        focused: false
1335                    },
1336                    ZoomItem {
1337                        name: "thisone".to_string(),
1338                        focused: true
1339                    },
1340                    ZoomItem {
1341                        name: "not".to_string(),
1342                        focused: false
1343                    },
1344                    ZoomItem {
1345                        name: "this".to_string(),
1346                        focused: false
1347                    },
1348                ]
1349            ))
1350        );
1351
1352        assert!(parse_zoom("blah").is_err());
1353    }
1354
1355    #[test]
1356    fn test_parse_target_list() {
1357        assert_eq!(parse_target_list(""), Ok(("", vec![])));
1358        assert_eq!(parse_target_list("    "), Ok(("", vec![])));
1359        assert_eq!(parse_target_list("a b c"), Ok(("", vec!["a", "b", "c"])));
1360        assert_eq!(
1361            parse_target_list("  a  \n\n   b\n   c\n\n"),
1362            Ok(("", vec!["a", "b", "c"]))
1363        );
1364        // should not consume the ending brace
1365        assert_eq!(parse_target_list("}"), Ok(("}", vec![])));
1366        assert_eq!(parse_target_list("a b c }"), Ok(("}", vec!["a", "b", "c"])));
1367    }
1368
1369    #[test]
1370    fn test_parse_glob() {
1371        assert_eq!(
1372            parse_input_command("glob a/b/**/*"),
1373            Ok(("", InputCommand::Glob("a/b/**/*".into())))
1374        );
1375
1376        assert_eq!(
1377            parse_input_command("glob a/x/**/*.h # should consume whitespace\n\n  \n\t\n  "),
1378            Ok(("", InputCommand::Glob("a/x/**/*.h".into())))
1379        );
1380    }
1381
1382    #[test]
1383    fn test_parse_include_dir() {
1384        assert_eq!(
1385            parse_input_command("include_dir a/b/**/*"),
1386            Ok(("", InputCommand::IncludeDirectory("a/b/**/*".into())))
1387        );
1388
1389        assert_eq!(
1390            parse_input_command("include_dir a/x/**/*.h # should consume whitespace\n\n  \n\t\n  "),
1391            Ok(("", InputCommand::IncludeDirectory("a/x/**/*.h".into())))
1392        );
1393    }
1394
1395    #[test]
1396    fn test_parse_compiledb() {
1397        assert_eq!(
1398            parse_compiledb("from compiledb foo load include_dirs"),
1399            Ok((
1400                "",
1401                InputCommand::LoadCompileDb {
1402                    path: "foo".into(),
1403                    load_include_directories: true,
1404                    load_sources: false
1405                }
1406            ))
1407        );
1408
1409        assert_eq!(
1410            parse_compiledb("from compiledb bar load sources"),
1411            Ok((
1412                "",
1413                InputCommand::LoadCompileDb {
1414                    path: "bar".into(),
1415                    load_include_directories: false,
1416                    load_sources: true
1417                }
1418            ))
1419        );
1420
1421        assert_eq!(
1422            parse_compiledb("from compiledb bar load sources, include_dirs, sources"),
1423            Ok((
1424                "",
1425                InputCommand::LoadCompileDb {
1426                    path: "bar".into(),
1427                    load_include_directories: true,
1428                    load_sources: true
1429                }
1430            ))
1431        );
1432
1433        assert_eq!(
1434            parse_compiledb(
1435                "
1436                # This is a comment (and whitespace) prefix
1437                from compiledb x/y/z load sources, sources\n# space/comment suffix\n"
1438            ),
1439            Ok((
1440                "",
1441                InputCommand::LoadCompileDb {
1442                    path: "x/y/z".into(),
1443                    load_include_directories: false,
1444                    load_sources: true
1445                }
1446            ))
1447        );
1448
1449        assert_eq!(
1450            parse_compiledb(
1451                "
1452                from compiledb x/y/z load sources, sources\nremaining"
1453            ),
1454            Ok((
1455                "remaining",
1456                InputCommand::LoadCompileDb {
1457                    path: "x/y/z".into(),
1458                    load_include_directories: false,
1459                    load_sources: true
1460                }
1461            ))
1462        );
1463    }
1464
1465    #[test]
1466    fn test_parse_input() {
1467        assert_eq!(
1468            parse_input(
1469                "input {
1470           from compiledb some_compile_db.json load include_dirs
1471           include_dir foo
1472           from compiledb some_compile_db.json load sources
1473
1474           glob xyz/**/*
1475
1476           include_dir bar
1477           from compiledb another.json load sources, include_dirs
1478
1479           glob final/**/*
1480           glob blah/**/*
1481        }"
1482            ),
1483            Ok((
1484                "",
1485                vec![
1486                    InputCommand::LoadCompileDb {
1487                        path: "some_compile_db.json".into(),
1488                        load_include_directories: true,
1489                        load_sources: false
1490                    },
1491                    InputCommand::IncludeDirectory("foo".into()),
1492                    InputCommand::LoadCompileDb {
1493                        path: "some_compile_db.json".into(),
1494                        load_include_directories: false,
1495                        load_sources: true
1496                    },
1497                    InputCommand::Glob("xyz/**/*".into()),
1498                    InputCommand::IncludeDirectory("bar".into()),
1499                    InputCommand::LoadCompileDb {
1500                        path: "another.json".into(),
1501                        load_include_directories: true,
1502                        load_sources: true
1503                    },
1504                    InputCommand::Glob("final/**/*".into()),
1505                    InputCommand::Glob("blah/**/*".into()),
1506                ]
1507            ))
1508        );
1509    }
1510
1511    #[test]
1512    fn test_variable_assignments() {
1513        assert_eq!(
1514            parse_variable_assignments(
1515                "
1516             a = b
1517             x=y
1518             z=${a}${x}
1519             ab=test
1520             other=${a${a}}ing
1521           "
1522            ),
1523            {
1524                let mut expected = HashMap::new();
1525                expected.insert("a".into(), "b".into());
1526                expected.insert("x".into(), "y".into());
1527                expected.insert("z".into(), "by".into());
1528                expected.insert("ab".into(), "test".into());
1529                expected.insert("other".into(), "testing".into());
1530                Ok(("", expected))
1531            }
1532        );
1533    }
1534
1535    #[test]
1536    fn test_expand_var() {
1537        let mut vars = HashMap::new();
1538        vars.insert("foo".into(), "bar".into());
1539        vars.insert("another".into(), "one".into());
1540        vars.insert("test".into(), "1234".into());
1541        vars.insert("theone".into(), "final".into());
1542
1543        assert_eq!(expand_variable("xyz", &vars), "xyz");
1544        assert_eq!(expand_variable("${foo}", &vars), "bar");
1545        assert_eq!(expand_variable("${another}", &vars), "one");
1546        assert_eq!(
1547            expand_variable("${foo}/${another}/${foo}", &vars),
1548            "bar/one/bar"
1549        );
1550        assert_eq!(expand_variable("${the${another}}", &vars), "final");
1551    }
1552}