use std::fmt::{self, Display};
use std::path::{Path, PathBuf};
use std::string::ToString;
use serde::de::{self, Deserializer, Error as SerdeError, Visitor};
use serde::Deserialize;
pub type CompilationDatabase = Vec<CompileCommand>;
#[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>,
{
#[allow(dead_code)]
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")),
}
}
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub enum CompileArgs {
Arguments(Vec<String>),
Flags(Vec<String>),
}
impl<'de> Deserialize<'de> for CompileArgs {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[allow(dead_code)]
struct CompileArgVisitor;
impl<'de> Visitor<'de> for CompileArgVisitor {
type Value = CompileArgs;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string representing a command line argument")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'de>,
{
let mut args = Vec::new();
while let Some(arg) = seq.next_element::<String>()? {
args.push(arg);
}
Ok(CompileArgs::Arguments(args))
}
}
deserializer.deserialize_seq(CompileArgVisitor)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct CompileCommand {
pub directory: PathBuf,
pub file: SourceFile,
pub arguments: Option<CompileArgs>,
pub command: Option<String>,
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())?;
match &self.arguments {
Some(CompileArgs::Arguments(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])?;
}
}
Some(CompileArgs::Flags(flags)) => {
write!(f, "\"flags\": [")?;
if flags.is_empty() {
writeln!(f, "],")?;
} else {
for flag in flags.iter().take(flags.len() - 1) {
writeln!(f, "\"{flag}\", ")?;
}
writeln!(f, "\"{}\"],", flags[flags.len() - 1])?;
}
}
None => {}
}
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 {
pub fn args_from_cmd(&self) -> Option<Vec<String>> {
let escaped = if let Some(ref cmd) = self.command {
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)
}
}
#[must_use]
pub fn from_compile_flags_txt(directory: &Path, contents: &str) -> CompilationDatabase {
let args = CompileArgs::Flags(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);
}
}