compile_commands/
lib.rs

1use std::fmt::{self, Display};
2use std::path::{Path, PathBuf};
3use std::string::ToString;
4
5use serde::de::{self, Deserializer, Error as SerdeError, Visitor};
6use serde::Deserialize;
7
8/// Represents a `compile_commands.json` file
9pub type CompilationDatabase = Vec<CompileCommand>;
10
11/// `All` if `CompilationDatabase` is generated from a `compile_flags.txt` file,
12/// otherwise `File()` containing the `file` field from a `compile_commands.json`
13/// entry
14#[derive(Debug, Clone, Hash, Eq, PartialEq)]
15pub enum SourceFile {
16    All,
17    File(PathBuf),
18}
19
20impl<'de> Deserialize<'de> for SourceFile {
21    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
22    where
23        D: Deserializer<'de>,
24    {
25        #[allow(dead_code)]
26        struct SourceFileVisitor;
27
28        impl<'de> Visitor<'de> for SourceFileVisitor {
29            type Value = SourceFile;
30
31            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
32                formatter.write_str("a string representing a file path")
33            }
34
35            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
36            where
37                E: SerdeError,
38            {
39                Ok(SourceFile::File(PathBuf::from(value)))
40            }
41        }
42
43        match serde_json::Value::deserialize(deserializer)? {
44            serde_json::Value::String(s) => Ok(SourceFile::File(PathBuf::from(s))),
45            _ => Err(SerdeError::custom("expected a string")),
46        }
47    }
48}
49
50/// The `arguments` field in a `compile_commands.json` file can be invoked as is,
51/// whereas the flags from a `compile_flags.txt` file must be invoked with a compiler,
52/// e.g. gcc @compile_flags.txt. Because the `CompileCommand` struct is used to
53/// represent both file types, we utilize a tagged union here to differentitate
54/// between the two files
55#[derive(Debug, Clone, Hash, Eq, PartialEq)]
56pub enum CompileArgs {
57    Arguments(Vec<String>),
58    Flags(Vec<String>),
59}
60
61impl<'de> Deserialize<'de> for CompileArgs {
62    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63    where
64        D: Deserializer<'de>,
65    {
66        #[allow(dead_code)]
67        struct CompileArgVisitor;
68
69        impl<'de> Visitor<'de> for CompileArgVisitor {
70            type Value = CompileArgs;
71
72            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
73                formatter.write_str("a string representing a command line argument")
74            }
75
76            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
77            where
78                A: de::SeqAccess<'de>,
79            {
80                let mut args = Vec::new();
81
82                while let Some(arg) = seq.next_element::<String>()? {
83                    args.push(arg);
84                }
85
86                Ok(CompileArgs::Arguments(args))
87            }
88        }
89
90        deserializer.deserialize_seq(CompileArgVisitor)
91    }
92}
93
94/// Represents a single entry within a `compile_commands.json` file, or a compile_flags.txt file
95/// Either `arguments` or `command` is required. `arguments` is preferred, as shell (un)escaping
96/// is a possible source of errors.
97///
98/// See: <https://clang.llvm.org/docs/JSONCompilationDatabase.html#format>
99#[derive(Debug, Clone, Deserialize)]
100pub struct CompileCommand {
101    /// The working directory of the compilation. All paths specified in the `command`
102    /// or `file` fields must be either absolute or relative to this directory.
103    pub directory: PathBuf,
104    /// The main translation unit source processed by this compilation step. This
105    /// is used by tools as the key into the compilation database. There can be
106    /// multiple command objects for the same file, for example if the same source
107    /// file is compiled with different configurations.
108    pub file: SourceFile,
109    /// The compile command argv as list of strings. This should run the compilation
110    /// step for the translation unit file. arguments[0] should be the executable
111    /// name, such as clang++. Arguments should not be escaped, but ready to pass
112    /// to execvp().
113    pub arguments: Option<CompileArgs>,
114    /// The compile command as a single shell-escaped string. Arguments may be
115    /// shell quoted and escaped following platform conventions, with ‘"’ and ‘\’
116    /// being the only special characters. Shell expansion is not supported.
117    pub command: Option<String>,
118    /// The name of the output created by this compilation step. This field is optional.
119    /// It can be used to distinguish different processing modes of the same input
120    /// file.
121    pub output: Option<PathBuf>,
122}
123
124impl Display for CompileCommand {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        writeln!(f, "{{ \"directory\": \"{}\",", self.directory.display())?;
127
128        match &self.arguments {
129            Some(CompileArgs::Arguments(arguments)) => {
130                write!(f, "\"arguments\": [")?;
131                if arguments.is_empty() {
132                    writeln!(f, "],")?;
133                } else {
134                    for arg in arguments.iter().take(arguments.len() - 1) {
135                        writeln!(f, "\"{arg}\", ")?;
136                    }
137                    writeln!(f, "\"{}\"],", arguments[arguments.len() - 1])?;
138                }
139            }
140            Some(CompileArgs::Flags(flags)) => {
141                write!(f, "\"flags\": [")?;
142                if flags.is_empty() {
143                    writeln!(f, "],")?;
144                } else {
145                    for flag in flags.iter().take(flags.len() - 1) {
146                        writeln!(f, "\"{flag}\", ")?;
147                    }
148                    writeln!(f, "\"{}\"],", flags[flags.len() - 1])?;
149                }
150            }
151            None => {}
152        }
153
154        if let Some(command) = &self.command {
155            write!(f, "\"command\": \"{command}\"")?;
156        }
157
158        if let Some(output) = &self.output {
159            writeln!(f, "\"output\": \"{}\"", output.display())?;
160        }
161
162        match &self.file {
163            SourceFile::All => write!(f, "\"file\": all }}")?,
164            SourceFile::File(file) => write!(f, "\"file\": \"{}\" }}", file.display())?,
165        }
166
167        Ok(())
168    }
169}
170
171impl CompileCommand {
172    /// Transforms the command field, if present, into a `Vec<String>` of equivalent
173    /// arguments
174    ///
175    /// Replaces escaped '"' and '\' characters with their respective literals
176    pub fn args_from_cmd(&self) -> Option<Vec<String>> {
177        let escaped = if let Some(ref cmd) = self.command {
178            // "Arguments may be shell quoted and escaped following platform conventions,
179            // with ‘"’ and ‘\’ being the only special characters."
180            cmd.trim().replace("\\\\", "\\").replace("\\\"", "\"")
181        } else {
182            return None;
183        };
184
185        let mut args = Vec::new();
186        let mut start: usize = 0;
187        let mut end: usize = 0;
188        let mut in_quotes = false;
189
190        for c in escaped.chars() {
191            if c == '"' {
192                in_quotes = !in_quotes;
193                end += 1;
194            } else if c.is_whitespace() && !in_quotes && start != end {
195                args.push(escaped[start..end].to_string());
196                end += 1;
197                start = end;
198            } else {
199                end += 1;
200            }
201        }
202
203        if start != end {
204            args.push(escaped[start..end].to_string());
205        }
206
207        Some(args)
208    }
209}
210
211/// For simple projects, Clang tools also recognize a `compile_flags.txt` file.
212/// This should contain one argument per line. The same flags will be used to
213/// compile any file.
214///
215/// See: <https://clang.llvm.org/docs/JSONCompilationDatabase.html#alternatives>
216///
217/// This helper allows you to translate the contents of a `compile_flags.txt` file
218/// to a `CompilationDatabase` object
219#[must_use]
220pub fn from_compile_flags_txt(directory: &Path, contents: &str) -> CompilationDatabase {
221    let args = CompileArgs::Flags(contents.lines().map(ToString::to_string).collect());
222    vec![CompileCommand {
223        directory: directory.to_path_buf(),
224        file: SourceFile::All,
225        arguments: Some(args),
226        command: None,
227        output: None,
228    }]
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    fn test_args_from_cmd(comp_cmd: &CompileCommand, expected_args: &Vec<&str>) {
236        let translated_args = comp_cmd.args_from_cmd().unwrap();
237
238        assert!(expected_args.len() == translated_args.len());
239        for (expected, actual) in expected_args.iter().zip(translated_args.iter()) {
240            assert!(expected == actual);
241        }
242    }
243
244    #[test]
245    fn it_translates_args_from_empty_cmd() {
246        let comp_cmd = CompileCommand {
247            directory: PathBuf::new(),
248            file: SourceFile::All,
249            arguments: None,
250            command: Some(String::from("")),
251            output: None,
252        };
253
254        let expected_args: Vec<&str> = Vec::new();
255        test_args_from_cmd(&comp_cmd, &expected_args);
256    }
257
258    #[test]
259    fn it_translates_args_from_cmd_1() {
260        let comp_cmd = CompileCommand {
261            directory: PathBuf::new(),
262            file: SourceFile::All,
263            arguments: None,
264            command: Some(String::from(
265                r#"/usr/bin/clang++ -Irelative -DSOMEDEF=\"With spaces, quotes and \\-es.\" -c -o file.o file.cc"#,
266            )),
267            output: None,
268        };
269
270        let expected_args: Vec<&str> = vec![
271            "/usr/bin/clang++",
272            "-Irelative",
273            r#"-DSOMEDEF="With spaces, quotes and \-es.""#,
274            "-c",
275            "-o",
276            "file.o",
277            "file.cc",
278        ];
279        test_args_from_cmd(&comp_cmd, &expected_args);
280    }
281}