use std::collections::BTreeMap;
use std::path::Path;
use clang::{Clang, Entity, EntityKind, EntityVisitResult, Index};
use codehelion_helper::CompileCommandSelector;
use codehelion_helper::ir::{
Anchor, CallSite, CallTarget, CompilerIr, Instantiation, ResolvedSymbol, SemanticConstruct,
SourceRange, SymbolKind, Unavailability, UnitRef, spell,
};
use codehelion_helper::protocol::Capability;
use crate::cfg_dump;
use crate::database::{Database, canonical};
use self::semantic::standard_api_name;
use self::type_table::TypeTable;
mod semantic;
mod type_table;
fn refused(unit: &UnitRef, why: &str) {
crate::refused(&format!("{}: {why}", unit.unit));
}
pub(crate) enum Outcome {
Analyzed(Box<CompilerIr>),
Unavailable(Unavailability),
}
pub(crate) fn analyze(
clang: &Clang,
unit: &UnitRef,
database: &Database,
selector: Option<&CompileCommandSelector>,
read_boundary: Option<&Path>,
want: &[Capability],
) -> Outcome {
let Some(entry) = database.unit(&unit.unit, selector) else {
refused(
unit,
if selector.is_some() {
"the compilation database has no command matching the one this unit was \
recorded with"
} else {
"the compilation database lists no command for this unit"
},
);
return Outcome::Unavailable(Unavailability::NoBuildInformation);
};
let arguments = match entry.arguments() {
Ok(arguments) => arguments,
Err(why) => {
refused(unit, why);
return Outcome::Unavailable(Unavailability::NoBuildInformation);
}
};
if let Some(boundary) = read_boundary
&& !arguments.reads_within(boundary)
{
refused(
unit,
&format!(
"the recorded command reads a file outside the scanned tree at {}",
boundary.display()
),
);
return Outcome::Unavailable(Unavailability::NoBuildInformation);
}
let index = Index::new(clang, false, false);
let parsed = match index
.parser(&entry.file)
.arguments(arguments.as_slice())
.detailed_preprocessing_record(true)
.skip_function_bodies(false)
.parse()
{
Ok(parsed) => parsed,
Err(error) => {
refused(
unit,
&format!("clang read no translation unit from the recorded command: {error}"),
);
return Outcome::Unavailable(Unavailability::NoBuildInformation);
}
};
let file = canonical(Path::new(&unit.file));
if parsed.get_file(&file).is_none() {
refused(
unit,
&format!(
"the unit was read and does not include {}, so there is no such pair to answer \
about",
file.display()
),
);
return Outcome::Unavailable(Unavailability::NoBuildInformation);
}
let mut reading = Reading::new(&database.root);
reading.walk(parsed.get_entity());
let cfg = want
.contains(&Capability::MirCfg)
.then(|| cfg_dump::produce(&entry.file, arguments, &reading.functions))
.flatten();
let mut ir = CompilerIr::empty(unit.clone());
ir.anchored_at = Some(database.root.display().to_string());
ir.symbols = reading.symbols;
ir.calls = reading.calls;
ir.semantic_constructs = reading.semantic_constructs;
ir.effects = codehelion_helper::effects::summarize(&ir.semantic_constructs);
ir.instantiations = reading.instantiations;
ir.types = reading.types.into_vec();
ir.cfg = cfg;
Outcome::Analyzed(Box::new(ir))
}
struct Reading<'a> {
files: Files<'a>,
macros: Vec<MacroStamp>,
types: TypeTable,
symbols: Vec<ResolvedSymbol>,
calls: Vec<CallSite>,
semantic_constructs: Vec<SemanticConstruct>,
instantiations: Vec<Instantiation>,
functions: Vec<cfg_dump::FunctionAnchor>,
}
pub(super) struct Files<'a> {
root: &'a Path,
known: BTreeMap<(u64, u64, u64), Spelled>,
}
impl<'a> Files<'a> {
const fn new(root: &'a Path) -> Self {
Self {
root,
known: BTreeMap::new(),
}
}
fn of(&mut self, file: &clang::source::File<'_>) -> &Spelled {
let root = self.root;
self.known.entry(file.get_id()).or_insert_with(|| {
let path = canonical(&file.get_path());
Spelled {
inside: path.starts_with(root),
name: spell(Some(root), &path),
}
})
}
}
struct Spelled {
name: String,
inside: bool,
}
struct MacroStamp {
file: (u64, u64, u64),
start: u64,
end: u64,
anchor: Anchor,
}
impl<'a> Reading<'a> {
fn new(root: &'a Path) -> Self {
Self {
files: Files::new(root),
macros: Vec::new(),
types: TypeTable::default(),
symbols: Vec::new(),
calls: Vec::new(),
semantic_constructs: Vec::new(),
instantiations: Vec::new(),
functions: Vec::new(),
}
}
fn known(&mut self, file: &clang::source::File<'_>) -> &Spelled {
self.files.of(file)
}
fn walk(&mut self, root: Entity<'_>) {
root.visit_children(|entity, _| {
if entity.get_kind() == EntityKind::MacroExpansion {
self.remember_macro(entity);
}
EntityVisitResult::Recurse
});
root.visit_children(|entity, parent| {
self.remember_function(entity);
self.remember_instantiation(entity, parent);
self.remember_call(entity);
self.remember_plain_range_collection(entity);
self.remember_plain_range_reduce(entity);
self.remember_fallible_validation(entity);
self.remember_expected_identity_propagation(entity);
self.remember_direct_lock_lifetime(entity);
self.visit(entity);
EntityVisitResult::Recurse
});
self.calls.sort_by(|left, right| {
anchor_order(&left.anchor, &right.anchor)
.then_with(|| call_target_order(&left.target, &right.target))
});
self.calls.dedup();
self.semantic_constructs.sort_by(|left, right| {
(
&left.anchor.expansion.file,
left.anchor.expansion.start_byte,
left.anchor.expansion.end_byte,
left.kind.name(),
)
.cmp(&(
&right.anchor.expansion.file,
right.anchor.expansion.start_byte,
right.anchor.expansion.end_byte,
right.kind.name(),
))
});
self.semantic_constructs.dedup();
self.instantiations.sort_by(|left, right| {
(
&left.anchor.expansion.file,
left.anchor.expansion.start_byte,
left.anchor.expansion.end_byte,
&left.instantiation_key,
)
.cmp(&(
&right.anchor.expansion.file,
right.anchor.expansion.start_byte,
right.anchor.expansion.end_byte,
&right.instantiation_key,
))
});
self.instantiations.dedup_by(|left, right| {
left.anchor.expansion == right.anchor.expansion
&& left.instantiation_key == right.instantiation_key
});
self.functions.sort_by(|left, right| {
(
&left.name,
&left.anchor.expansion.file,
left.anchor.expansion.start_byte,
)
.cmp(&(
&right.name,
&right.anchor.expansion.file,
right.anchor.expansion.start_byte,
))
});
self.functions.dedup_by(|left, right| {
left.name == right.name && left.anchor.expansion == right.anchor.expansion
});
}
fn remember_function(&mut self, entity: Entity<'_>) {
if !cfg_callable(entity)
|| !entity
.get_children()
.iter()
.any(|child| child.get_kind() == EntityKind::CompoundStmt)
{
return;
}
let (Some(name), Some(anchor)) = (entity.get_name(), self.anchor(entity)) else {
return;
};
self.functions
.push(cfg_dump::FunctionAnchor { name, anchor });
}
fn remember_call(&mut self, entity: Entity<'_>) {
if entity.get_kind() != EntityKind::CallExpr {
return;
}
let Some(anchor) = self.anchor(entity) else {
return;
};
let reference = (!entity.is_dynamic_call())
.then(|| entity.get_reference())
.flatten()
.map(|target| target.get_canonical_entity())
.filter(|target| callable(target.get_kind()));
let api_name = reference
.as_ref()
.and_then(|target| standard_api_name(*target));
let target = reference
.and_then(|target| target.get_usr())
.map_or(CallTarget::Unresolved, |symbol| CallTarget::Static {
symbol: symbol.0,
});
self.calls.push(CallSite {
anchor,
target,
api_name,
});
}
fn remember_instantiation(&mut self, entity: Entity<'_>, parent: Entity<'_>) {
let (specialization, argument_type) = match entity.get_kind() {
EntityKind::DeclRefExpr => {
let Some(specialization) = entity.get_reference() else {
return;
};
(specialization, None)
}
EntityKind::TemplateRef => {
let Some(ty) = parent.get_type().map(|ty| ty.get_canonical_type()) else {
return;
};
let Some(specialization) = ty.get_declaration() else {
return;
};
(specialization, Some(ty))
}
_ => return,
};
let Some(origin) = specialization.get_template() else {
return;
};
let origin = origin.get_canonical_entity();
if self.is_external(origin) || !same_definition_site(specialization, origin) {
return;
}
let (Some(definition), Some(specialization_usr)) =
(origin.get_usr(), specialization.get_usr())
else {
return;
};
let Some(origin_range) = self.definition_range(origin) else {
return;
};
let Some(mut anchor) = self.anchor(entity) else {
return;
};
anchor.definition = Some(origin_range);
let arguments = argument_type.map_or_else(Vec::new, |ty| {
let (types, files) = (&mut self.types, &mut self.files);
ty.get_template_argument_types()
.unwrap_or_default()
.into_iter()
.flatten()
.map(|argument| types.intern(argument, files))
.collect()
});
self.instantiations.push(Instantiation {
anchor,
definition: definition.0,
definition_end_line: definition_end_line(origin),
artifact_match_key: specialization_display_key(specialization),
instantiation_key: format!("clang-usr-v1:{}", specialization_usr.0),
arguments,
});
}
fn remember_macro(&mut self, expansion: Entity<'_>) {
let Some(definition) = expansion
.get_reference()
.or_else(|| expansion.get_definition())
.filter(|entity| entity.get_kind() == EntityKind::MacroDefinition)
else {
return;
};
let Some(invocation) = expansion.get_range() else {
return;
};
let start = invocation.get_start().get_expansion_location();
let end = invocation.get_end().get_expansion_location();
let (Some(start_file), Some(end_file)) = (start.file, end.file) else {
return;
};
if start_file.get_id() != end_file.get_id() || end.offset <= start.offset {
return;
}
let Some(written) = self.definition_range(definition) else {
return;
};
let file_name = self.known(&start_file).name.clone();
self.macros.push(MacroStamp {
file: start_file.get_id(),
start: u64::from(start.offset),
end: u64::from(end.offset),
anchor: Anchor {
expansion: SourceRange {
file: file_name,
start_byte: u64::from(start.offset),
end_byte: u64::from(end.offset),
start_line: start.line,
},
definition: Some(written),
},
});
}
fn definition_range(&mut self, definition: Entity<'_>) -> Option<SourceRange> {
let range = definition.get_range()?;
let start = range.get_start().get_spelling_location();
let end = range.get_end().get_spelling_location();
let (start_file, end_file) = (start.file?, end.file?);
if start_file.get_id() != end_file.get_id() || end.offset <= start.offset {
return None;
}
Some(SourceRange {
file: self.known(&start_file).name.clone(),
start_byte: u64::from(start.offset),
end_byte: u64::from(end.offset),
start_line: start.line,
})
}
fn visit(&mut self, entity: Entity<'_>) {
let Some(anchor) = self.anchor(entity) else {
return;
};
let named = entity.get_reference().unwrap_or(entity);
let Some(kind) = symbol_kind(named.get_kind()) else {
return;
};
let Some(name) = entity.get_name().or_else(|| named.get_name()) else {
return;
};
let type_index = named
.get_type()
.map(|ty| self.types.intern(ty, &mut self.files));
let external = self.is_external(named);
let id = identity(named, &mut self.files);
self.symbols.push(ResolvedSymbol {
id,
name,
kind,
anchor,
type_index,
external,
});
}
fn anchor(&mut self, entity: Entity<'_>) -> Option<Anchor> {
let at = entity.get_location()?.get_expansion_location();
let file = at.file?;
if !self.known(&file).inside {
return None;
}
let offset = u64::from(at.offset);
if let Some(anchor) = self
.macros
.iter()
.filter(|stamp| {
stamp.file == file.get_id() && stamp.start <= offset && offset <= stamp.end
})
.min_by_key(|stamp| stamp.end - stamp.start)
.map(|stamp| stamp.anchor.clone())
{
return Some(anchor);
}
let range = entity.get_range()?;
let start = range.get_start().get_expansion_location();
let end = range.get_end().get_expansion_location();
let known = self.known(&start.file?);
if !known.inside {
return None;
}
let expansion = SourceRange {
file: known.name.clone(),
start_byte: u64::from(start.offset),
end_byte: u64::from(end.offset.max(start.offset)),
start_line: start.line,
};
Some(Anchor::written_here(expansion))
}
fn scope_end_anchor(&mut self, scope: Entity<'_>) -> Option<Anchor> {
let range = scope.get_range()?;
let end = range.get_end().get_expansion_location();
let file = end.file?;
let known = self.known(&file);
if !known.inside {
return None;
}
Some(Anchor::written_here(SourceRange {
file: known.name.clone(),
start_byte: u64::from(end.offset),
end_byte: u64::from(end.offset),
start_line: end.line,
}))
}
fn is_external(&mut self, entity: Entity<'_>) -> bool {
let Some(location) = entity.get_location() else {
return true;
};
if location.is_in_system_header() {
return true;
}
let Some(file) = location.get_expansion_location().file else {
return true;
};
!self.known(&file).inside
}
}
fn definition_end_line(definition: Entity<'_>) -> Option<u32> {
let range = definition.get_range()?;
let start = range.get_start().get_expansion_location();
let end = range.get_end().get_expansion_location();
(start.file?.get_id() == end.file?.get_id()).then_some(end.line)
}
fn specialization_display_key(specialization: Entity<'_>) -> Option<String> {
let display = specialization.get_display_name()?;
let mut parents = Vec::new();
let mut parent = specialization.get_semantic_parent();
while let Some(current) = parent {
if matches!(
current.get_kind(),
EntityKind::Namespace | EntityKind::StructDecl | EntityKind::ClassDecl
) && let Some(name) = current.get_name()
&& !name.is_empty()
{
parents.push(name);
}
parent = current.get_semantic_parent();
}
parents.reverse();
parents.push(display);
Some(format!("clang-display-v1:{}", parents.join("::")))
}
fn same_definition_site(specialization: Entity<'_>, origin: Entity<'_>) -> bool {
let Some(specialized) = specialization.get_location() else {
return false;
};
let Some(original) = origin.get_location() else {
return false;
};
let specialized = specialized.get_spelling_location();
let original = original.get_spelling_location();
let (Some(specialized_file), Some(original_file)) = (specialized.file, original.file) else {
return false;
};
specialized_file.get_id() == original_file.get_id() && specialized.offset == original.offset
}
fn anchor_order(left: &Anchor, right: &Anchor) -> std::cmp::Ordering {
source_range_order(&left.expansion, &right.expansion).then_with(|| {
match (&left.definition, &right.definition) {
(Some(left), Some(right)) => source_range_order(left, right),
(None, None) => std::cmp::Ordering::Equal,
(None, Some(_)) => std::cmp::Ordering::Less,
(Some(_), None) => std::cmp::Ordering::Greater,
}
})
}
fn source_range_order(left: &SourceRange, right: &SourceRange) -> std::cmp::Ordering {
(
left.file.as_str(),
left.start_byte,
left.end_byte,
left.start_line,
)
.cmp(&(
right.file.as_str(),
right.start_byte,
right.end_byte,
right.start_line,
))
}
fn call_target_order(left: &CallTarget, right: &CallTarget) -> std::cmp::Ordering {
match (left, right) {
(CallTarget::Static { symbol: left }, CallTarget::Static { symbol: right }) => {
left.cmp(right)
}
(CallTarget::Dynamic { candidates: left }, CallTarget::Dynamic { candidates: right }) => {
left.cmp(right)
}
(CallTarget::Unresolved, CallTarget::Unresolved) => std::cmp::Ordering::Equal,
(CallTarget::Static { .. }, _) | (CallTarget::Dynamic { .. }, CallTarget::Unresolved) => {
std::cmp::Ordering::Less
}
(_, CallTarget::Static { .. }) | (CallTarget::Unresolved, CallTarget::Dynamic { .. }) => {
std::cmp::Ordering::Greater
}
}
}
pub(super) fn identity(entity: Entity<'_>, files: &mut Files<'_>) -> String {
if let Some(usr) = entity.get_usr() {
return usr.0;
}
let name = entity.get_name().unwrap_or_default();
let Some(location) = entity.get_location() else {
return name;
};
let at = location.get_expansion_location();
let Some(file) = at.file else {
return name;
};
format!("{name}@{}:{}", files.of(&file).name, at.offset)
}
fn cfg_callable(entity: Entity<'_>) -> bool {
cfg_callable_kind(
entity.get_kind(),
entity.get_semantic_parent().map(|parent| parent.get_kind()),
)
}
fn cfg_callable_kind(kind: EntityKind, semantic_parent: Option<EntityKind>) -> bool {
callable(kind)
&& kind != EntityKind::FunctionTemplate
&& !matches!(
semantic_parent,
Some(EntityKind::ClassTemplate | EntityKind::ClassTemplatePartialSpecialization)
)
}
const fn callable(kind: EntityKind) -> bool {
matches!(
kind,
EntityKind::FunctionDecl
| EntityKind::Method
| EntityKind::Constructor
| EntityKind::Destructor
| EntityKind::ConversionFunction
| EntityKind::FunctionTemplate
)
}
const fn symbol_kind(kind: EntityKind) -> Option<SymbolKind> {
Some(match kind {
EntityKind::FunctionDecl
| EntityKind::Method
| EntityKind::Constructor
| EntityKind::Destructor
| EntityKind::ConversionFunction
| EntityKind::FunctionTemplate => SymbolKind::Function,
EntityKind::StructDecl
| EntityKind::UnionDecl
| EntityKind::ClassDecl
| EntityKind::EnumDecl
| EntityKind::TypedefDecl
| EntityKind::TypeAliasDecl
| EntityKind::TypeAliasTemplateDecl
| EntityKind::ClassTemplate
| EntityKind::ClassTemplatePartialSpecialization
| EntityKind::TemplateTypeParameter => SymbolKind::Type,
EntityKind::FieldDecl => SymbolKind::Field,
EntityKind::EnumConstantDecl => SymbolKind::Variant,
EntityKind::ParmDecl | EntityKind::VarDecl | EntityKind::NonTypeTemplateParameter => {
SymbolKind::Binding
}
EntityKind::Namespace | EntityKind::NamespaceAlias => SymbolKind::Namespace,
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cfgs_are_not_requested_for_template_definitions() {
assert!(cfg_callable_kind(EntityKind::FunctionDecl, None));
assert!(!cfg_callable_kind(EntityKind::FunctionTemplate, None));
assert!(!cfg_callable_kind(
EntityKind::Method,
Some(EntityKind::ClassTemplate)
));
assert!(!cfg_callable_kind(
EntityKind::Method,
Some(EntityKind::ClassTemplatePartialSpecialization)
));
}
}