use std::collections::HashSet;
pub(crate) struct FileSource {
pub path: String,
pub content: String,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum CommentSyntax {
CStyle,
Hash,
}
fn comment_syntax_for(path: &str) -> CommentSyntax {
match Path::new(path).extension().and_then(|e| e.to_str()) {
Some("py" | "rb") => CommentSyntax::Hash,
_ => CommentSyntax::CStyle,
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum MaskState {
Code,
LineComment,
BlockComment,
StringLiteral,
}
fn is_ident_start(b: u8) -> bool {
b.is_ascii_alphabetic() || b == b'_'
}
fn is_ident_char(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
fn mask_non_code(content: &str, syntax: CommentSyntax) -> Vec<u8> {
let bytes = content.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut state = MaskState::Code;
let mut i = 0usize;
while i < bytes.len() {
if bytes[i] == b'\n' {
out.push(b'\n');
if state == MaskState::LineComment || state == MaskState::StringLiteral {
state = MaskState::Code;
}
i += 1;
continue;
}
i += match state {
MaskState::Code => step_code(bytes, i, syntax, &mut state, &mut out),
MaskState::LineComment => blank(&mut out, 1),
MaskState::BlockComment => step_block_comment(bytes, i, &mut state, &mut out),
MaskState::StringLiteral => step_string(bytes, i, &mut state, &mut out),
};
}
out
}
fn blank(out: &mut Vec<u8>, n: usize) -> usize {
out.extend(std::iter::repeat_n(b' ', n));
n
}
fn step_code(
bytes: &[u8],
i: usize,
syntax: CommentSyntax,
state: &mut MaskState,
out: &mut Vec<u8>,
) -> usize {
if let Some((opened, width)) = opening_delimiter(bytes[i], bytes.get(i + 1).copied(), syntax) {
*state = opened;
return blank(out, width);
}
out.push(bytes[i]);
1
}
fn opening_delimiter(
b: u8,
next: Option<u8>,
syntax: CommentSyntax,
) -> Option<(MaskState, usize)> {
match (syntax, b, next) {
(CommentSyntax::CStyle, b'/', Some(b'/')) => Some((MaskState::LineComment, 2)),
(CommentSyntax::CStyle, b'/', Some(b'*')) => Some((MaskState::BlockComment, 2)),
(CommentSyntax::Hash, b'#', _) => Some((MaskState::LineComment, 1)),
(_, b'"', _) => Some((MaskState::StringLiteral, 1)),
_ => None,
}
}
fn step_block_comment(bytes: &[u8], i: usize, state: &mut MaskState, out: &mut Vec<u8>) -> usize {
if bytes[i] == b'*' && bytes.get(i + 1).copied() == Some(b'/') {
*state = MaskState::Code;
return blank(out, 2);
}
blank(out, 1)
}
fn step_string(bytes: &[u8], i: usize, state: &mut MaskState, out: &mut Vec<u8>) -> usize {
if bytes[i] == b'\\' && bytes.get(i + 1).is_some_and(|&n| n != b'\n') {
return blank(out, 2);
}
if bytes[i] == b'"' {
*state = MaskState::Code;
}
blank(out, 1)
}
const BINDING_KEYWORDS: [&str; 13] = [
"fn",
"struct",
"enum",
"class",
"def",
"function",
"interface",
"trait",
"mod",
"let",
"const",
"static",
"type",
];
fn skip_whitespace_back(masked: &[u8], mut i: usize) -> Option<usize> {
while i > 0 && masked[i - 1].is_ascii_whitespace() {
i -= 1;
}
(i > 0).then_some(i)
}
fn is_member_access(masked: &[u8], start: usize, member_arrow: bool) -> bool {
let Some(i) = skip_whitespace_back(masked, start) else {
return false;
};
if masked[i - 1] == b'.' {
return !(i >= 2 && masked[i - 2] == b'.');
}
member_arrow && masked[i - 1] == b'>' && i >= 2 && masked[i - 2] == b'-'
}
fn follows_binding_keyword(masked: &[u8], start: usize) -> bool {
let Some(end) = skip_whitespace_back(masked, start) else {
return false;
};
let mut begin = end;
while begin > 0 && is_ident_char(masked[begin - 1]) {
begin -= 1;
}
begin != end
&& std::str::from_utf8(&masked[begin..end])
.is_ok_and(|word| BINDING_KEYWORDS.contains(&word))
}
struct Occurrence {
line: usize,
column: usize,
start: usize,
end: usize,
is_call: bool,
}
fn starts_attribute(masked: &[u8], from: usize) -> bool {
let mut i = from;
while i < masked.len() && (masked[i] == b' ' || masked[i] == b'\t') {
i += 1;
}
if masked.get(i) != Some(&b'#') {
return false;
}
let after = masked.get(i + 1);
after == Some(&b'[') || (after == Some(&b'!') && masked.get(i + 2) == Some(&b'['))
}
fn opens_call(masked: &[u8], end: usize) -> bool {
let mut i = end;
while i < masked.len() && (masked[i] == b' ' || masked[i] == b'\t') {
i += 1;
}
masked.get(i) == Some(&b'(')
}
fn is_plausible_use(symbol: &Symbol, occ: &Occurrence) -> bool {
match symbol.kind {
SymbolKind::Function | SymbolKind::Method => occ.is_call,
_ => true,
}
}
fn collect_known_occurrences(
masked: &[u8],
known: &DefinitionIndex,
member_arrow: bool,
) -> Vec<Occurrence> {
let mut found = Vec::new();
let mut line = 1usize;
let mut line_start = 0usize;
let mut attribute_line = starts_attribute(masked, 0);
let mut i = 0usize;
while i < masked.len() {
let b = masked[i];
if b == b'\n' {
line += 1;
i += 1;
line_start = i;
attribute_line = starts_attribute(masked, line_start);
continue;
}
if !is_ident_start(b) {
i += 1;
continue;
}
let start = i;
while i < masked.len() && is_ident_char(masked[i]) {
i += 1;
}
if !attribute_line && is_use_candidate(masked, start, i, known, member_arrow) {
found.push(Occurrence {
line,
column: start - line_start,
start,
end: i,
is_call: opens_call(masked, i),
});
}
}
found
}
fn is_use_candidate(
masked: &[u8],
start: usize,
end: usize,
known: &DefinitionIndex,
member_arrow: bool,
) -> bool {
known.is_known(&masked[start..end])
&& !is_member_access(masked, start, member_arrow)
&& !follows_binding_keyword(masked, start)
}
fn uses_arrow_member_access(path: &str) -> bool {
matches!(
Path::new(path).extension().and_then(|e| e.to_str()),
Some("c" | "cpp" | "h" | "hpp")
)
}
fn build_include_units(files: &[FileSource]) -> HashMap<String, String> {
let index_of: HashMap<PathBuf, usize> = files
.iter()
.enumerate()
.map(|(i, f)| (normalize_path(Path::new(&f.path)), i))
.collect();
let mut parent: Vec<usize> = (0..files.len()).collect();
for (i, file) in files.iter().enumerate() {
let dir = Path::new(&file.path).parent().map(Path::to_path_buf);
for target in included_paths(&file.content) {
let Some(dir) = dir.as_ref() else { continue };
let resolved = normalize_path(&dir.join(target));
if let Some(&j) = index_of.get(&resolved) {
union(&mut parent, i, j);
}
}
}
files
.iter()
.enumerate()
.map(|(i, f)| (f.path.clone(), files[find(&mut parent, i)].path.clone()))
.collect()
}
fn find(parent: &mut [usize], mut i: usize) -> usize {
while parent[i] != i {
parent[i] = parent[parent[i]];
i = parent[i];
}
i
}
fn union(parent: &mut [usize], a: usize, b: usize) {
let (ra, rb) = (find(parent, a), find(parent, b));
if ra != rb {
let (lo, hi) = if ra < rb { (ra, rb) } else { (rb, ra) };
parent[hi] = lo;
}
}
fn included_paths(content: &str) -> Vec<String> {
use regex::Regex;
use std::sync::OnceLock;
static RE: OnceLock<Regex> = OnceLock::new();
let re = RE.get_or_init(|| {
Regex::new(r#"include!\s*\(\s*"([^"]+)"\s*\)"#).expect("static regex must compile")
});
re.captures_iter(content)
.filter_map(|c| c.get(1).map(|m| m.as_str().to_string()))
.collect()
}
fn normalize_path(path: &Path) -> PathBuf {
use std::path::Component;
let mut out = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
out.pop();
}
other => out.push(other.as_os_str()),
}
}
out
}
struct DefinitionIndex {
by_name: HashMap<String, Vec<usize>>,
exported_by_name: HashMap<String, Vec<usize>>,
by_unit: HashMap<String, FileDefinitions>,
sites_by_file: HashMap<String, HashSet<(usize, usize)>>,
unit_of: HashMap<String, String>,
}
fn is_exported(symbol: &Symbol) -> bool {
matches!(symbol.visibility, Visibility::Public | Visibility::Protected)
}
#[derive(Default)]
struct FileDefinitions {
by_name: HashMap<String, Vec<usize>>,
}
impl DefinitionIndex {
fn is_known(&self, ident: &[u8]) -> bool {
std::str::from_utf8(ident).is_ok_and(|name| self.by_name.contains_key(name))
}
fn unit_definitions(&self, path: &str) -> Option<&FileDefinitions> {
self.unit_of.get(path).and_then(|u| self.by_unit.get(u))
}
fn is_definition_site(&self, path: &str, line: usize, column: usize) -> bool {
self.sites_by_file
.get(path)
.is_some_and(|s| s.contains(&(line, column)))
}
}
fn build_definition_index(files: &[FileSource], symbols: &[Symbol]) -> DefinitionIndex {
let unit_of = build_include_units(files);
let mut by_name: HashMap<String, Vec<usize>> = HashMap::new();
let mut exported_by_name: HashMap<String, Vec<usize>> = HashMap::new();
let mut by_unit: HashMap<String, FileDefinitions> = HashMap::new();
let mut sites_by_file: HashMap<String, HashSet<(usize, usize)>> = HashMap::new();
for (idx, symbol) in symbols.iter().enumerate() {
by_name.entry(symbol.name.clone()).or_default().push(idx);
if is_exported(symbol) {
exported_by_name
.entry(symbol.name.clone())
.or_default()
.push(idx);
}
let unit = unit_of
.get(&symbol.file)
.cloned()
.unwrap_or_else(|| symbol.file.clone());
by_unit
.entry(unit)
.or_default()
.by_name
.entry(symbol.name.clone())
.or_default()
.push(idx);
sites_by_file
.entry(symbol.file.clone())
.or_default()
.insert((symbol.line, symbol.column));
}
DefinitionIndex {
by_name,
exported_by_name,
by_unit,
sites_by_file,
unit_of,
}
}
pub(crate) fn resolve_references(files: &[FileSource], symbols: &mut [Symbol]) -> HashSet<String> {
let index = build_definition_index(files, symbols);
let mut unresolved: HashSet<String> = HashSet::new();
for file in files {
let masked = mask_non_code(&file.content, comment_syntax_for(&file.path));
let occurrences =
collect_known_occurrences(&masked, &index, uses_arrow_member_access(&file.path));
let local = index.unit_definitions(&file.path);
for occ in occurrences {
if index.is_definition_site(&file.path, occ.line, occ.column) {
continue; }
let Ok(name) = std::str::from_utf8(&masked[occ.start..occ.end]) else {
continue;
};
attribute_use(&index, local, symbols, file, &occ, name, &mut unresolved);
}
}
unresolved
}
fn attribute_use(
index: &DefinitionIndex,
local: Option<&FileDefinitions>,
symbols: &mut [Symbol],
file: &FileSource,
occ: &Occurrence,
name: &str,
unresolved: &mut HashSet<String>,
) {
let reference = Reference {
file: file.path.clone(),
line: occ.line,
column: occ.column,
kind: ReferenceKind::Usage,
};
if let Some(indices) = local.and_then(|defs| defs.by_name.get(name)) {
for idx in plausible(symbols, indices, occ) {
symbols[idx].references.push(reference.clone());
}
return;
}
let Some(candidates) = index.exported_by_name.get(name) else {
unresolved.insert(name.to_string());
return;
};
let matching = plausible(symbols, candidates, occ);
match matching.len() {
0 => {}
1 => symbols[matching[0]].references.push(reference),
_ => {
unresolved.insert(name.to_string());
}
}
}
fn plausible(symbols: &[Symbol], candidates: &[usize], occ: &Occurrence) -> Vec<usize> {
candidates
.iter()
.copied()
.filter(|&idx| is_plausible_use(&symbols[idx], occ))
.collect()
}
pub(crate) fn usage_count(symbol: &Symbol) -> usize {
symbol
.references
.iter()
.filter(|r| !matches!(r.kind, ReferenceKind::Definition))
.count()
}