#![allow(
clippy::enum_glob_use,
clippy::if_not_else,
clippy::too_many_lines,
clippy::wildcard_imports
)]
use std::collections::{HashMap, HashSet, hash_map};
use std::path::{Path, PathBuf};
use petgraph::{
Direction,
algo::{kosaraju_scc, toposort},
graph::NodeIndex,
stable_graph::StableGraph,
visit::{Dfs, NodeIndexable},
};
use serde::{Deserialize, Serialize};
use crate::c_langs_macros::is_specials;
use crate::langs::*;
use crate::languages::language_preproc::*;
use crate::node::{Cursor, Node};
use crate::tools::*;
use crate::traits::*;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub enum PreprocDiagnostic {
SelfInclusion {
file: PathBuf,
},
IncludeCycle {
members: Vec<String>,
},
NonUtf8CyclePath {
path: String,
},
NonUtf8IndirectInclude {
path: String,
},
NotPreprocessed {
file: PathBuf,
},
}
impl std::fmt::Display for PreprocDiagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SelfInclusion { file } => {
write!(f, "possible self inclusion {}", file.display())
}
Self::IncludeCycle { members } => {
write!(f, "possible include cycle:")?;
for member in members {
write!(f, "\n - \"{member}\"")?;
}
Ok(())
}
Self::NonUtf8CyclePath { path } => {
write!(f, "skipping non-UTF-8 path in include cycle: {path}")
}
Self::NonUtf8IndirectInclude { path } => {
write!(f, "skipping non-UTF-8 indirect include path: {path}")
}
Self::NotPreprocessed { file } => write!(
f,
"included file which has not been preprocessed: {}",
file.display()
),
}
}
}
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct PreprocFile {
pub direct_includes: HashSet<String>,
pub indirect_includes: HashSet<String>,
pub macros: HashSet<String>,
}
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct PreprocResults {
pub files: HashMap<PathBuf, PreprocFile>,
}
impl PreprocFile {
#[must_use]
pub fn new_macros(macros: &[&str]) -> Self {
let mut pf = Self::default();
for m in macros {
pf.macros.insert((*m).to_string());
}
pf
}
}
crate::observation::counter!(owned_macro_sets);
pub fn get_macros<S: ::std::hash::BuildHasher>(
file: &Path,
files: &HashMap<PathBuf, PreprocFile, S>,
) -> HashSet<String> {
owned_macro_sets::record();
visible_macros(file, files)
.into_iter()
.map(ToOwned::to_owned)
.collect()
}
pub(crate) fn visible_macros<'a, S: ::std::hash::BuildHasher>(
file: &Path,
files: &'a HashMap<PathBuf, PreprocFile, S>,
) -> HashSet<&'a str> {
let mut macros = HashSet::new();
let Some(pf) = files.get(file) else {
return macros;
};
macros.extend(pf.macros.iter().map(String::as_str));
for include in &pf.indirect_includes {
if let Some(included) = files.get(Path::new(include)) {
macros.extend(included.macros.iter().map(String::as_str));
}
}
macros
}
type IncludeGraph = StableGraph<PathBuf, i32>;
fn ensure_node(
g: &mut IncludeGraph,
nodes: &mut HashMap<PathBuf, NodeIndex>,
file: &Path,
) -> NodeIndex {
match nodes.entry(file.to_path_buf()) {
hash_map::Entry::Occupied(l) => *l.get(),
hash_map::Entry::Vacant(p) => *p.insert(g.add_node(file.to_path_buf())),
}
}
fn resolve_single_include<S: ::std::hash::BuildHasher>(
file: &Path,
include: &str,
all_files: &HashMap<String, Vec<PathBuf>, S>,
) -> Option<PathBuf> {
guess_file(file, include, all_files).into_iter().min()
}
fn build_include_graph<S: ::std::hash::BuildHasher>(
files: &HashMap<PathBuf, PreprocFile, S>,
all_files: &HashMap<String, Vec<PathBuf>, S>,
diagnostics: &mut Vec<PreprocDiagnostic>,
) -> (IncludeGraph, HashMap<PathBuf, NodeIndex>) {
let mut nodes: HashMap<PathBuf, NodeIndex> = HashMap::new();
let mut g = StableGraph::new();
for (file, pf) in files {
let node = ensure_node(&mut g, &mut nodes, file);
for i in &pf.direct_includes {
let Some(included) = resolve_single_include(file, i, all_files) else {
continue;
};
if &included == file {
diagnostics.push(PreprocDiagnostic::SelfInclusion { file: file.clone() });
continue;
}
let included = ensure_node(&mut g, &mut nodes, &included);
g.add_edge(node, included, 0);
}
}
(g, nodes)
}
fn scc_external_neighbors(
g: &IncludeGraph,
component: &[NodeIndex],
direction: Direction,
) -> Vec<NodeIndex> {
let mut neighbors = Vec::new();
for c in component {
for n in g.neighbors_directed(*c, direction) {
if !component.contains(&n) && !neighbors.contains(&n) {
neighbors.push(n);
}
}
}
neighbors
}
fn collapse_scc(
g: &mut IncludeGraph,
nodes: &mut HashMap<PathBuf, NodeIndex>,
diagnostics: &mut Vec<PreprocDiagnostic>,
) -> HashMap<NodeIndex, HashSet<String>> {
let mut scc = kosaraju_scc(&*g);
let mut scc_map: HashMap<NodeIndex, HashSet<String>> = HashMap::new();
for component in &mut scc {
if component.len() > 1 {
let (replacement, paths) = collapse_one_component(g, nodes, diagnostics, component);
scc_map.insert(replacement, paths);
}
}
scc_map
}
fn collapse_one_component(
g: &mut IncludeGraph,
nodes: &mut HashMap<PathBuf, NodeIndex>,
diagnostics: &mut Vec<PreprocDiagnostic>,
component: &mut Vec<NodeIndex>,
) -> (NodeIndex, HashSet<String>) {
let incoming = scc_external_neighbors(g, component, Direction::Incoming);
let outgoing = scc_external_neighbors(g, component, Direction::Outgoing);
let mut paths = HashSet::new();
let replacement = g.add_node(PathBuf::from(""));
for i in incoming {
g.add_edge(i, replacement, 0);
}
for o in outgoing {
g.add_edge(replacement, o, 0);
}
for c in component.drain(..) {
let path = g
.remove_node(c)
.expect("invariant: SCC component node must exist in graph");
if let Some(s) = path.to_str() {
paths.insert(s.to_string());
} else {
diagnostics.push(PreprocDiagnostic::NonUtf8CyclePath {
path: path.display().to_string(),
});
}
*nodes
.get_mut(&path)
.expect("invariant: every graph node must have a nodes map entry") = replacement;
}
let mut members: Vec<String> = paths.iter().cloned().collect();
members.sort_unstable();
diagnostics.push(PreprocDiagnostic::IncludeCycle { members });
(replacement, paths)
}
crate::observation::counter!(include_graph_walks);
enum NodeContribution<'a> {
Path(&'a str),
NonUtf8(&'a Path),
}
struct IncludeClosures<'a> {
entries: Vec<NodeContribution<'a>>,
closures: Vec<Vec<usize>>,
}
impl IncludeClosures<'_> {
fn materialize(
&self,
start: NodeIndex,
x_inc: &mut HashSet<String>,
diagnostics: &mut Vec<PreprocDiagnostic>,
) {
let Some(ids) = self.closures.get(start.index()) else {
return;
};
x_inc.reserve(ids.len());
for entry in ids.iter().filter_map(|&id| self.entries.get(id)) {
match entry {
NodeContribution::Path(path) => {
x_inc.insert((*path).to_string());
}
NodeContribution::NonUtf8(path) => {
diagnostics.push(PreprocDiagnostic::NonUtf8IndirectInclude {
path: path.display().to_string(),
});
}
}
}
}
}
fn merge_sorted_ids(a: &[usize], b: &[usize], out: &mut Vec<usize>) {
out.clear();
out.reserve(a.len() + b.len());
let (mut i, mut j) = (0, 0);
while let (Some(&left), Some(&right)) = (a.get(i), b.get(j)) {
out.push(left.min(right));
if left <= right {
i += 1;
}
if right <= left {
j += 1;
}
}
out.extend_from_slice(&a[i..]);
out.extend_from_slice(&b[j..]);
}
fn index_node_contributions<'a>(
g: &'a IncludeGraph,
scc_map: &'a HashMap<NodeIndex, HashSet<String>>,
) -> (Vec<NodeContribution<'a>>, Vec<std::ops::Range<usize>>) {
let mut entries = Vec::with_capacity(g.node_count());
let mut own = vec![0..0; g.node_bound()];
for node in g.node_indices() {
let start = entries.len();
match g.node_weight(node) {
Some(weight) if weight.as_os_str().is_empty() => {
if let Some(paths) = scc_map.get(&node) {
entries.extend(paths.iter().map(|p| NodeContribution::Path(p)));
}
}
Some(weight) => entries.push(
weight
.to_str()
.map_or_else(|| NodeContribution::NonUtf8(weight), NodeContribution::Path),
),
None => {}
}
own[node.index()] = start..entries.len();
}
(entries, own)
}
fn compute_include_closures<'a>(
g: &'a IncludeGraph,
scc_map: &'a HashMap<NodeIndex, HashSet<String>>,
) -> Option<IncludeClosures<'a>> {
let order = toposort(g, None).ok()?;
include_graph_walks::record();
let (entries, own) = index_node_contributions(g, scc_map);
let mut closures: Vec<Vec<usize>> = vec![Vec::new(); g.node_bound()];
let mut merged = Vec::new();
for node in order.into_iter().rev() {
let mut acc: Vec<usize> = own[node.index()].clone().collect();
for succ in g.neighbors_directed(node, Direction::Outgoing) {
merge_sorted_ids(&acc, &closures[succ.index()], &mut merged);
std::mem::swap(&mut acc, &mut merged);
}
closures[node.index()] = acc;
}
Some(IncludeClosures { entries, closures })
}
fn record_indirect_includes<S: ::std::hash::BuildHasher>(
files: &mut HashMap<PathBuf, PreprocFile, S>,
g: &IncludeGraph,
nodes: &HashMap<PathBuf, NodeIndex>,
scc_map: &HashMap<NodeIndex, HashSet<String>>,
diagnostics: &mut Vec<PreprocDiagnostic>,
) {
let precomputed = compute_include_closures(g, scc_map);
for (path, start) in nodes {
let Some(pf) = files.get_mut(path) else {
diagnostics.push(PreprocDiagnostic::NotPreprocessed { file: path.clone() });
continue;
};
if let Some(closures) = &precomputed {
closures.materialize(*start, &mut pf.indirect_includes, diagnostics);
} else {
accumulate_reachable_includes(
g,
*start,
scc_map,
&mut pf.indirect_includes,
diagnostics,
);
}
}
}
fn accumulate_reachable_includes(
g: &IncludeGraph,
start: NodeIndex,
scc_map: &HashMap<NodeIndex, HashSet<String>>,
x_inc: &mut HashSet<String>,
diagnostics: &mut Vec<PreprocDiagnostic>,
) {
include_graph_walks::record();
let mut dfs = Dfs::new(g, start);
while let Some(node) = dfs.next(g) {
let w = g
.node_weight(node)
.expect("invariant: DFS-visited node must have weight in graph");
if w.as_os_str().is_empty() {
let paths = scc_map.get(&node).expect(
"every empty-path node is an SCC replacement and must have a scc_map entry",
);
x_inc.extend(paths.iter().cloned());
} else if let Some(s) = w.to_str() {
x_inc.insert(s.to_string());
} else {
diagnostics.push(PreprocDiagnostic::NonUtf8IndirectInclude {
path: w.display().to_string(),
});
}
}
}
pub fn fix_includes<S: ::std::hash::BuildHasher>(
files: &mut HashMap<PathBuf, PreprocFile, S>,
all_files: &HashMap<String, Vec<PathBuf>, S>,
) -> Vec<PreprocDiagnostic> {
let mut diagnostics = Vec::new();
let (mut g, mut nodes) = build_include_graph(files, all_files, &mut diagnostics);
let scc_map = collapse_scc(&mut g, &mut nodes, &mut diagnostics);
record_indirect_includes(files, &g, &nodes, &scc_map, &mut diagnostics);
diagnostics
}
fn strip_include_quotes(code: &[u8], start: usize, end: usize) -> Option<&str> {
const MIN_QUOTED_LEN: usize = 2;
if end < start + MIN_QUOTED_LEN {
return None;
}
let inner = &code[start + 1..end - 1];
let first = inner.iter().position(|&c| c != b' ' && c != b'\t')?;
let last = inner.iter().rposition(|&c| c != b' ' && c != b'\t')?;
std::str::from_utf8(&inner[first..=last]).ok()
}
pub fn preprocess(source: Vec<u8>, path: &Path, results: &mut PreprocResults) {
preprocess_with_parser(&PreprocParser::new(source, path, None), path, results);
}
pub(crate) fn preprocess_with_parser(
parser: &PreprocParser,
path: &Path,
results: &mut PreprocResults,
) {
let node = parser.root();
let mut cursor = node.cursor();
let code = parser.code();
let mut file_result = PreprocFile::default();
let mut macro_events: Vec<(usize, MacroEvent)> = Vec::new();
let mut stack = vec![node];
while let Some(node) = stack.pop() {
push_children(&mut cursor, &node, &mut stack);
classify_preproc_node(
&mut cursor,
&node,
code,
&mut file_result,
&mut macro_events,
);
}
apply_macro_events(macro_events, &mut file_result);
results.files.insert(path.to_path_buf(), file_result);
}
fn push_children<'a>(cursor: &mut Cursor<'a>, node: &Node<'a>, stack: &mut Vec<Node<'a>>) {
stack.extend(node.children_with(cursor));
}
fn classify_preproc_node<'a>(
cursor: &mut Cursor<'a>,
node: &Node<'a>,
code: &'a [u8],
file_result: &mut PreprocFile,
macro_events: &mut Vec<(usize, MacroEvent)>,
) {
let id = Preproc::from(node.kind_id());
match id {
Preproc::Define | Preproc::Undef => {
cursor.reset(node);
cursor.goto_first_child();
let identifier = cursor.node();
if identifier.kind_id() == Preproc::Identifier
&& let Some(macro_text) = identifier.utf8_text(code)
&& !is_specials(macro_text)
{
let event = if id == Preproc::Undef {
MacroEvent::Undef(macro_text.to_string())
} else {
MacroEvent::Define(macro_text.to_string())
};
macro_events.push((identifier.start_byte(), event));
}
}
Preproc::PreprocInclude => {
cursor.reset(node);
cursor.goto_first_child();
let file = cursor.node();
if file.kind_id() == Preproc::StringLiteral
&& let Some(include) =
strip_include_quotes(code, file.start_byte(), file.end_byte())
{
file_result.direct_includes.insert(include.to_string());
}
}
_ => {}
}
}
fn apply_macro_events(mut macro_events: Vec<(usize, MacroEvent)>, file_result: &mut PreprocFile) {
macro_events.sort_by_key(|(offset, _)| *offset);
for (_, event) in macro_events {
match event {
MacroEvent::Define(name) => {
file_result.macros.insert(name);
}
MacroEvent::Undef(name) => {
file_result.macros.remove(&name);
}
}
}
}
enum MacroEvent {
Define(String),
Undef(String),
}
#[cfg(test)]
#[path = "preproc_tests.rs"]
mod tests;