use anyhow::{Context, Result};
use serde::Deserialize;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use super::context::ProjectContext;
use crate::parser::CParser;
#[derive(Debug, Deserialize)]
struct RawEntry {
directory: String,
#[serde(default)]
command: Option<String>,
#[serde(default)]
arguments: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandLineDefine {
pub spelling: String,
pub body: String,
}
impl CommandLineDefine {
pub fn name(&self) -> &str {
match self.spelling.find('(') {
Some(i) => &self.spelling[..i],
None => &self.spelling,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CompileDb {
pub include_paths: Vec<String>,
pub defines: Vec<CommandLineDefine>,
pub entry_count: usize,
pub compilers: Vec<String>,
}
impl CompileDb {
pub fn load(path: &Path) -> Result<Self> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read compile database: {}", path.display()))?;
let entries: Vec<RawEntry> = serde_json::from_str(&text).with_context(|| {
format!(
"Failed to parse {} as a JSON compilation database (expected an array of \
{{directory, file, command|arguments}} objects)",
path.display()
)
})?;
Ok(Self::from_entries(&entries))
}
fn from_entries(entries: &[RawEntry]) -> Self {
let mut db = CompileDb {
entry_count: entries.len(),
..Default::default()
};
let mut seen_paths: HashSet<String> = HashSet::new();
let mut seen_compilers: HashSet<String> = HashSet::new();
let mut undefined: HashSet<String> = HashSet::new();
let mut seen_defines: HashSet<String> = HashSet::new();
for entry in entries {
let argv = match (&entry.arguments, &entry.command) {
(Some(args), _) => args.clone(),
(None, Some(cmd)) => split_command(cmd),
(None, None) => continue,
};
if argv.is_empty() {
continue;
}
if seen_compilers.insert(argv[0].clone()) {
db.compilers.push(argv[0].clone());
}
let base = Path::new(&entry.directory);
for flag in parse_flags(&argv) {
match flag {
Flag::Include(dir) => {
let abs = absolutize(base, &dir);
let s = abs.to_string_lossy().to_string();
if seen_paths.insert(s.clone()) {
db.include_paths.push(s);
}
}
Flag::Define(spelling, body) => {
if seen_defines.insert(spelling.clone()) {
db.defines.push(CommandLineDefine { spelling, body });
}
}
Flag::Undefine(name) => {
undefined.insert(name);
}
}
}
}
if !undefined.is_empty() {
db.defines.retain(|d| !undefined.contains(d.name()));
}
db
}
pub fn missing_include_paths(&self) -> Vec<&str> {
self.include_paths
.iter()
.filter(|p| !Path::new(p).is_dir())
.map(|p| p.as_str())
.collect()
}
pub fn define_directives(&self) -> String {
let mut out = String::new();
for d in &self.defines {
let body = if d.body.is_empty() { "1" } else { &d.body };
out.push_str("#define ");
out.push_str(&d.spelling);
out.push(' ');
out.push_str(body);
out.push('\n');
}
out
}
pub fn merge_defines_into(&self, context: &mut ProjectContext) -> Result<usize> {
if self.defines.is_empty() {
return Ok(0);
}
let source = self.define_directives();
let mut parser = CParser::new()?;
let (tree, source) = parser.parse_source(&source)?;
let root = tree.root_node();
let mut added = 0usize;
for (name, value) in super::const_eval::collect_macro_constants(&root, &source) {
if let std::collections::hash_map::Entry::Vacant(e) =
context.macro_constants.entry(name)
{
e.insert(value);
added += 1;
}
}
for (name, target) in super::const_eval::collect_macro_aliases(&root, &source) {
if let std::collections::hash_map::Entry::Vacant(e) = context.macro_aliases.entry(name)
{
e.insert(target);
added += 1;
}
}
for (name, m) in super::macro_expand::collect_function_macros(&root, &source) {
if let std::collections::hash_map::Entry::Vacant(e) =
context.function_macros.entry(name)
{
e.insert(m);
added += 1;
}
}
Ok(added)
}
}
#[derive(Debug, PartialEq, Eq)]
enum Flag {
Include(String),
Define(String, String),
Undefine(String),
}
const DIR_FLAGS: &[&str] = &["-I", "-isystem", "-iquote", "-idirafter"];
fn parse_flags(argv: &[String]) -> Vec<Flag> {
let mut out = Vec::new();
let mut i = 0;
while i < argv.len() {
let arg = argv[i].as_str();
i += 1;
if let Some(rest) = arg.strip_prefix("-D") {
if let Some(f) = define_flag(rest, argv, &mut i) {
out.push(f);
}
continue;
}
if let Some(rest) = arg.strip_prefix("-U") {
let name = take_value(rest, argv, &mut i);
if let Some(name) = name {
if !name.is_empty() {
out.push(Flag::Undefine(name));
}
}
continue;
}
let mut matched: Option<&str> = None;
for f in DIR_FLAGS {
if arg.starts_with(f) && matched.is_none_or(|m: &str| f.len() > m.len()) {
matched = Some(f);
}
}
if let Some(f) = matched {
let rest = &arg[f.len()..];
let rest = rest.strip_prefix('=').unwrap_or(rest);
if let Some(dir) = take_value(rest, argv, &mut i) {
if !dir.is_empty() {
out.push(Flag::Include(dir));
}
}
}
}
out
}
fn take_value(attached: &str, argv: &[String], i: &mut usize) -> Option<String> {
if !attached.is_empty() {
return Some(attached.to_string());
}
let next = argv.get(*i)?;
*i += 1;
Some(next.clone())
}
fn define_flag(attached: &str, argv: &[String], i: &mut usize) -> Option<Flag> {
let text = take_value(attached, argv, i)?;
if text.is_empty() {
return None;
}
let (spelling, body) = match text.find('=') {
Some(eq) => (text[..eq].to_string(), text[eq + 1..].to_string()),
None => (text, String::new()),
};
if spelling.is_empty() {
return None;
}
Some(Flag::Define(spelling, body))
}
fn absolutize(base: &Path, dir: &str) -> PathBuf {
let p = Path::new(dir);
if p.is_absolute() {
p.to_path_buf()
} else {
base.join(p)
}
}
fn split_command(cmd: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut has_token = false;
let mut chars = cmd.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\\' => {
if let Some(next) = chars.next() {
cur.push(next);
has_token = true;
}
}
'\'' => {
has_token = true;
for c in chars.by_ref() {
if c == '\'' {
break;
}
cur.push(c);
}
}
'"' => {
has_token = true;
while let Some(c) = chars.next() {
match c {
'"' => break,
'\\' => match chars.peek() {
Some('"') | Some('\\') | Some('$') | Some('`') => {
cur.push(chars.next().unwrap_or_default());
}
_ => cur.push('\\'),
},
_ => cur.push(c),
}
}
}
c if c.is_whitespace() => {
if has_token {
out.push(std::mem::take(&mut cur));
has_token = false;
}
}
_ => {
cur.push(c);
has_token = true;
}
}
}
if has_token {
out.push(cur);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn argv(parts: &[&str]) -> Vec<String> {
parts.iter().map(|s| s.to_string()).collect()
}
#[test]
fn parses_attached_and_separate_include_flags() {
let flags = parse_flags(&argv(&["cc", "-Iinc", "-I", "other", "-c", "a.c"]));
assert_eq!(
flags,
vec![Flag::Include("inc".into()), Flag::Include("other".into()),]
);
}
#[test]
fn parses_long_include_flag_forms() {
let flags = parse_flags(&argv(&[
"cc",
"-isystem",
"/usr/local/include",
"-iquote=q",
"-idirafter",
"after",
]));
assert_eq!(
flags,
vec![
Flag::Include("/usr/local/include".into()),
Flag::Include("q".into()),
Flag::Include("after".into()),
]
);
}
#[test]
fn parses_define_forms() {
let flags = parse_flags(&argv(&["cc", "-DFOO", "-DBAR=2", "-D", "BAZ=3"]));
assert_eq!(
flags,
vec![
Flag::Define("FOO".into(), String::new()),
Flag::Define("BAR".into(), "2".into()),
Flag::Define("BAZ".into(), "3".into()),
]
);
}
#[test]
fn function_like_define_keeps_parameter_list_in_spelling() {
let flags = parse_flags(&argv(&["cc", "-DMAX(a,b)=((a)>(b)?(a):(b))"]));
assert_eq!(
flags,
vec![Flag::Define("MAX(a,b)".into(), "((a)>(b)?(a):(b))".into())]
);
let d = CommandLineDefine {
spelling: "MAX(a,b)".into(),
body: "((a)>(b)?(a):(b))".into(),
};
assert_eq!(d.name(), "MAX");
}
#[test]
fn bare_define_renders_as_one() {
let db = CompileDb {
defines: vec![CommandLineDefine {
spelling: "FOO".into(),
body: String::new(),
}],
..Default::default()
};
assert_eq!(db.define_directives(), "#define FOO 1\n");
}
#[test]
fn undefine_removes_a_define_from_any_entry() {
let entries = vec![
RawEntry {
directory: "/p".into(),
command: Some("cc -DFOO=1 -DKEEP=2 -c a.c".into()),
arguments: None,
},
RawEntry {
directory: "/p".into(),
command: Some("cc -UFOO -c b.c".into()),
arguments: None,
},
];
let db = CompileDb::from_entries(&entries);
let names: Vec<&str> = db.defines.iter().map(|d| d.name()).collect();
assert_eq!(names, vec!["KEEP"]);
}
#[test]
fn relative_include_paths_resolve_against_entry_directory() {
let entries = vec![RawEntry {
directory: "/proj/build".into(),
command: Some("cc -I../src -I/abs/inc -c a.c".into()),
arguments: None,
}];
let db = CompileDb::from_entries(&entries);
assert_eq!(db.include_paths, vec!["/proj/build/../src", "/abs/inc"]);
}
#[test]
fn include_paths_dedupe_preserving_first_seen_order() {
let entries = vec![
RawEntry {
directory: "/p".into(),
command: Some("cc -Ia -Ib -c a.c".into()),
arguments: None,
},
RawEntry {
directory: "/p".into(),
command: Some("cc -Ib -Ic -c b.c".into()),
arguments: None,
},
];
let db = CompileDb::from_entries(&entries);
assert_eq!(db.include_paths, vec!["/p/a", "/p/b", "/p/c"]);
assert_eq!(db.entry_count, 2);
}
#[test]
fn arguments_form_wins_over_command_form() {
let entries = vec![RawEntry {
directory: "/p".into(),
command: Some("cc -Ifrom_command -c a.c".into()),
arguments: Some(argv(&["cc", "-Ifrom_arguments", "-c", "a.c"])),
}];
let db = CompileDb::from_entries(&entries);
assert_eq!(db.include_paths, vec!["/p/from_arguments"]);
}
#[test]
fn records_distinct_compilers() {
let entries = vec![
RawEntry {
directory: "/p".into(),
command: Some("/usr/bin/cc -c a.c".into()),
arguments: None,
},
RawEntry {
directory: "/p".into(),
command: Some("/usr/bin/cc -c b.c".into()),
arguments: None,
},
RawEntry {
directory: "/p".into(),
command: Some("arm-none-eabi-gcc -c c.c".into()),
arguments: None,
},
];
let db = CompileDb::from_entries(&entries);
assert_eq!(db.compilers, vec!["/usr/bin/cc", "arm-none-eabi-gcc"]);
}
#[test]
fn split_command_handles_quotes_and_escapes() {
assert_eq!(
split_command(r#"cc -DS=\"hi\" -I"/a b" -DT='x y' -c a.c"#),
argv(&["cc", r#"-DS="hi""#, "-I/a b", "-DT=x y", "-c", "a.c"])
);
}
#[test]
fn split_command_ignores_repeated_whitespace() {
assert_eq!(
split_command(" cc -c\ta.c "),
argv(&["cc", "-c", "a.c"])
);
}
#[test]
fn entries_without_a_command_are_skipped_not_fatal() {
let entries = vec![
RawEntry {
directory: "/p".into(),
command: None,
arguments: None,
},
RawEntry {
directory: "/p".into(),
command: Some("cc -Iinc -c a.c".into()),
arguments: None,
},
];
let db = CompileDb::from_entries(&entries);
assert_eq!(db.include_paths, vec!["/p/inc"]);
}
#[test]
fn merge_defines_does_not_override_source_derived_macros() {
let mut ctx = ProjectContext::new();
ctx.macro_constants.insert("BUFSZ".into(), 64);
let db = CompileDb {
defines: vec![
CommandLineDefine {
spelling: "BUFSZ".into(),
body: "999".into(),
},
CommandLineDefine {
spelling: "NEWSZ".into(),
body: "16".into(),
},
],
..Default::default()
};
db.merge_defines_into(&mut ctx).unwrap();
assert_eq!(ctx.macro_constants.get("BUFSZ"), Some(&64));
assert_eq!(ctx.macro_constants.get("NEWSZ"), Some(&16));
}
#[test]
fn merge_defines_contributes_function_like_macros() {
let mut ctx = ProjectContext::new();
let db = CompileDb {
defines: vec![CommandLineDefine {
spelling: "SQUARE(x)".into(),
body: "((x)*(x))".into(),
}],
..Default::default()
};
db.merge_defines_into(&mut ctx).unwrap();
assert!(ctx.function_macros.contains_key("SQUARE"));
}
#[test]
fn compile_db_include_path_brings_header_macros_into_context() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::create_dir_all(root.join("vendor/inc")).unwrap();
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(
root.join("vendor/inc/vlib.h"),
"#define VBUF_LEN 8\n#define VZERO(p) ((p)->a = 0)\n",
)
.unwrap();
let c_file = root.join("src/a.c");
std::fs::write(&c_file, "#include <vlib.h>\nint f(void) { return 0; }\n").unwrap();
let db_path = root.join("compile_commands.json");
std::fs::write(
&db_path,
format!(
r#"[{{"directory":"{d}","file":"{f}","command":"cc -Ivendor/inc -c src/a.c"}}]"#,
d = root.display(),
f = c_file.display(),
),
)
.unwrap();
let db = CompileDb::load(&db_path).unwrap();
assert_eq!(db.include_paths.len(), 1);
let mut ctx = ProjectContext::new();
assert!(!ctx.macro_constants.contains_key("VBUF_LEN"));
super::super::prescan::resolve_includes(
&[c_file.to_string_lossy().to_string()],
&db.include_paths,
&[root.to_string_lossy().to_string()],
&mut ctx,
None,
false,
)
.unwrap();
assert_eq!(
ctx.macro_constants.get("VBUF_LEN"),
Some(&8),
"compile-database -I path should make the vendored header's constants resolvable"
);
assert!(
ctx.function_macros.contains_key("VZERO"),
"compile-database -I path should make the vendored header's function-like macros expandable"
);
}
#[test]
fn missing_include_paths_flags_only_absent_directories() {
let dir = tempfile::tempdir().unwrap();
let present = dir.path().to_string_lossy().to_string();
let db = CompileDb {
include_paths: vec![present.clone(), "/definitely/not/here".into()],
..Default::default()
};
assert_eq!(db.missing_include_paths(), vec!["/definitely/not/here"]);
}
#[test]
fn merge_defines_is_a_noop_without_defines() {
let mut ctx = ProjectContext::new();
let db = CompileDb::default();
assert_eq!(db.merge_defines_into(&mut ctx).unwrap(), 0);
assert!(ctx.macro_constants.is_empty());
}
}