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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
use std::fmt::{self, Display};
use std::path::{Path, PathBuf};
use std::string::ToString;

use serde::de::{Deserializer, Error as SerdeError, Visitor};
use serde::Deserialize;

/// Represents a `compile_commands.json` file
pub type CompilationDatabase = Vec<CompileCommand>;

/// `All` if `CompilationDatabase` is generated from a `compile_flags.txt` file,
/// otherwise `File()` containing the `file` field from a `compile_commands.json`
/// entry
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub enum SourceFile {
    All,
    File(PathBuf),
}

impl<'de> Deserialize<'de> for SourceFile {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct SourceFileVisitor;

        impl<'de> Visitor<'de> for SourceFileVisitor {
            type Value = SourceFile;

            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
                formatter.write_str("a string representing a file path")
            }

            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
            where
                E: SerdeError,
            {
                Ok(SourceFile::File(PathBuf::from(value)))
            }
        }

        match serde_json::Value::deserialize(deserializer)? {
            serde_json::Value::String(s) => Ok(SourceFile::File(PathBuf::from(s))),
            _ => Err(SerdeError::custom("expected a string")),
        }
    }
}

/// Represents a single entry within a `compile_commands.json` file
/// Either `arguments` or `command` is required. `arguments` is preferred, as shell (un)escaping
/// is a possible source of errors.
///
/// See: <https://clang.llvm.org/docs/JSONCompilationDatabase.html#format>
#[derive(Debug, Clone, Deserialize)]
pub struct CompileCommand {
    /// The working directory of the compilation. All paths specified in the `command`
    /// or `file` fields must be either absolute or relative to this directory.
    pub directory: PathBuf,
    /// The main translation unit source processed by this compilation step. This
    /// is used by tools as the key into the compilation database. There can be
    /// multiple command objects for the same file, for example if the same source
    /// file is compiled with different configurations.
    pub file: SourceFile,
    /// The compile command argv as list of strings. This should run the compilation
    /// step for the translation unit file. arguments[0] should be the executable
    /// name, such as clang++. Arguments should not be escaped, but ready to pass
    /// to execvp().
    pub arguments: Option<Vec<String>>,
    /// The compile command as a single shell-escaped string. Arguments may be
    /// shell quoted and escaped following platform conventions, with ‘"’ and ‘\’
    /// being the only special characters. Shell expansion is not supported.
    pub command: Option<String>,
    /// The name of the output created by this compilation step. This field is optional.
    /// It can be used to distinguish different processing modes of the same input
    /// file.
    pub output: Option<PathBuf>,
}

impl Display for CompileCommand {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "{{ \"directory\": \"{}\",", self.directory.display())?;

        if let Some(arguments) = &self.arguments {
            write!(f, "\"arguments\": [")?;
            if arguments.is_empty() {
                writeln!(f, "],")?;
            } else {
                for arg in arguments.iter().take(arguments.len() - 1) {
                    writeln!(f, "\"{arg}\", ")?;
                }
                writeln!(f, "\"{}\"],", arguments[arguments.len() - 1])?;
            }
        }

        if let Some(command) = &self.command {
            write!(f, "\"command\": \"{command}\"")?;
        }

        if let Some(output) = &self.output {
            writeln!(f, "\"output\": \"{}\"", output.display())?;
        }

        match &self.file {
            SourceFile::All => write!(f, "\"file\": all }}")?,
            SourceFile::File(file) => write!(f, "\"file\": \"{}\" }}", file.display())?,
        }

        Ok(())
    }
}

impl CompileCommand {
    /// Transforms the command field, if present, into a `Vec<String>` of equivalent
    /// arguments
    ///
    /// Replaces escaped '"' and '\' characters with their respective literals
    pub fn args_from_cmd(&self) -> Option<Vec<String>> {
        let escaped = if let Some(ref cmd) = self.command {
            // "Arguments may be shell quoted and escaped following platform conventions,
            // with ‘"’ and ‘\’ being the only special characters."
            cmd.trim().replace("\\\\", "\\").replace("\\\"", "\"")
        } else {
            return None;
        };

        let mut args = Vec::new();
        let mut start: usize = 0;
        let mut end: usize = 0;
        let mut in_quotes = false;

        for c in escaped.chars() {
            if c == '"' {
                in_quotes = !in_quotes;
                end += 1;
            } else if c.is_whitespace() && !in_quotes && start != end {
                args.push(escaped[start..end].to_string());
                end += 1;
                start = end;
            } else {
                end += 1;
            }
        }

        if start != end {
            args.push(escaped[start..end].to_string());
        }

        Some(args)
    }
}

/// For simple projects, Clang tools also recognize a `compile_flags.txt` file.
/// This should contain one argument per line. The same flags will be used to
/// compile any file.
///
/// See: <https://clang.llvm.org/docs/JSONCompilationDatabase.html#alternatives>
///
/// This helper allows you to translate the contents of a `compile_flags.txt` file
/// to a `CompilationDatabase` object
#[must_use]
pub fn from_compile_flags_txt(directory: &Path, contents: &str) -> CompilationDatabase {
    let args = contents.lines().map(ToString::to_string).collect();
    vec![CompileCommand {
        directory: directory.to_path_buf(),
        file: SourceFile::All,
        arguments: Some(args),
        command: None,
        output: None,
    }]
}

#[cfg(test)]
mod tests {
    use super::*;

    fn test_args_from_cmd(comp_cmd: &CompileCommand, expected_args: &Vec<&str>) {
        let translated_args = comp_cmd.args_from_cmd().unwrap();

        assert!(expected_args.len() == translated_args.len());
        for (expected, actual) in expected_args.iter().zip(translated_args.iter()) {
            assert!(expected == actual);
        }
    }

    #[test]
    fn it_translates_args_from_empty_cmd() {
        let comp_cmd = CompileCommand {
            directory: PathBuf::new(),
            file: SourceFile::All,
            arguments: None,
            command: Some(String::from("")),
            output: None,
        };

        let expected_args: Vec<&str> = Vec::new();
        test_args_from_cmd(&comp_cmd, &expected_args);
    }

    #[test]
    fn it_translates_args_from_cmd_1() {
        let comp_cmd = CompileCommand {
            directory: PathBuf::new(),
            file: SourceFile::All,
            arguments: None,
            command: Some(String::from(
                r#"/usr/bin/clang++ -Irelative -DSOMEDEF=\"With spaces, quotes and \\-es.\" -c -o file.o file.cc"#,
            )),
            output: None,
        };

        let expected_args: Vec<&str> = vec![
            "/usr/bin/clang++",
            "-Irelative",
            r#"-DSOMEDEF="With spaces, quotes and \-es.""#,
            "-c",
            "-o",
            "file.o",
            "file.cc",
        ];
        test_args_from_cmd(&comp_cmd, &expected_args);
    }
}