include_graph/dependencies/
cparse.rs

1use super::error::Error;
2
3use regex::Regex;
4use std::{
5    fmt::Debug,
6    path::{Path, PathBuf},
7    sync::Arc,
8};
9use tokio::{
10    fs::File,
11    io::{AsyncBufReadExt as _, BufReader},
12    task::JoinSet,
13};
14use tracing::{error, info, trace};
15
16/// Attempt to make the full path of head::tail
17/// returns None if that fails (e.g. path does not exist)
18fn try_resolve(head: &Path, tail: &PathBuf) -> Option<PathBuf> {
19    head.join(tail).canonicalize().ok()
20}
21
22#[derive(PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
23pub enum FileType {
24    Header,
25    Source,
26    Unknown,
27}
28
29impl FileType {
30    pub fn of(path: &Path) -> Self {
31        let ext = path
32            .extension()
33            .and_then(|e| e.to_str())
34            .unwrap_or("")
35            .to_lowercase();
36        match ext.as_str() {
37            "h" | "hpp" => FileType::Header,
38            "c" | "cpp" | "cc" | "cxx" => FileType::Source,
39            _ => FileType::Unknown,
40        }
41    }
42}
43
44/// Given a C-like source, try to resolve includes.
45///
46/// Includes are generally of the form `#include <name>` or `#include "name"`
47pub async fn extract_includes(
48    path: &PathBuf,
49    include_dirs: &[PathBuf],
50) -> Result<Vec<PathBuf>, Error> {
51    let f = File::open(path).await.map_err(|source| Error::IOError {
52        source,
53        path: path.clone(),
54        message: "open",
55    })?;
56
57    let reader = BufReader::new(f);
58
59    let inc_re = Regex::new(r##"^\s*#include\s*(["<])([^">]*)[">]"##).unwrap();
60
61    let mut result = Vec::new();
62    let parent_dir = PathBuf::from(path.parent().unwrap());
63
64    let mut lines = reader.lines();
65
66    loop {
67        let line = lines.next_line().await.map_err(|source| Error::IOError {
68            source,
69            path: path.clone(),
70            message: "line read",
71        })?;
72
73        let line = match line {
74            Some(value) => value,
75            None => break,
76        };
77
78        if let Some(captures) = inc_re.captures(&line) {
79            let inc_type = captures.get(1).unwrap().as_str();
80            let relative_path = PathBuf::from(captures.get(2).unwrap().as_str());
81
82            trace!("Possible include: {:?}", relative_path);
83
84            if inc_type == "\"" {
85                if let Some(p) = try_resolve(&parent_dir, &relative_path) {
86                    result.push(p);
87                    continue;
88                }
89            }
90
91            if let Some(p) = include_dirs
92                .iter()
93                .find_map(|i| try_resolve(i, &relative_path))
94            {
95                result.push(p);
96            } else {
97                // Debug only as this is VERY common due to C++ and system inclues,
98                // like "list", "vector", "string" or even platform specific like "jni.h"
99                // or non-enabled things (like openthread on a non-thread platform)
100                trace!("Include {:?} could not be resolved", relative_path);
101            }
102        }
103    }
104
105    info!(target: "include-extract",
106          "Includes for:\n  {:?}: {:#?}", path, result);
107
108    Ok(result)
109}
110
111#[derive(Debug, PartialEq, PartialOrd)]
112pub struct SourceWithIncludes {
113    pub path: PathBuf,
114    pub includes: Vec<PathBuf>,
115}
116
117/// Given a list of paths, figure out their dependencies
118pub async fn all_sources_and_includes<I, E>(
119    paths: I,
120    includes: &[PathBuf],
121) -> Result<Vec<SourceWithIncludes>, Error>
122where
123    I: Iterator<Item = Result<PathBuf, E>>,
124    E: Debug,
125{
126    let includes = Arc::new(Vec::from(includes));
127
128    let mut join_set = JoinSet::new();
129
130    for entry in paths {
131        let path = match entry {
132            Ok(value) => value.canonicalize().map_err(|e| Error::Internal {
133                message: format!("{:?}", e),
134            })?,
135            Err(e) => {
136                return Err(Error::Internal {
137                    message: format!("{:?}", e),
138                })
139            }
140        };
141
142        if FileType::of(&path) == FileType::Unknown {
143            trace!("Skipping non-source: {:?}", path);
144            continue;
145        }
146
147        // prepare data to mve into sub-task
148        let includes = includes.clone();
149
150        join_set.spawn(async move {
151            trace!("PROCESS: {:?}", path);
152            let includes = match extract_includes(&path, &includes).await {
153                Ok(value) => value,
154                Err(e) => {
155                    error!("Error extracing includes: {:?}", e);
156                    return Err(e);
157                }
158            };
159
160            Ok(SourceWithIncludes { path, includes })
161        });
162    }
163
164    let mut results = Vec::new();
165    while let Some(h) = join_set.join_next().await {
166        let r = h.map_err(Error::JoinError)?;
167        results.push(r?)
168    }
169
170    Ok(results)
171}