use std::path::{Component, Path, PathBuf};
use anyhow::Context;
use clap::ArgMatches;
use qli_analyzers::DefRefs;
use qli_core::{AnalysisResult, LanguageId};
use qli_scip::{IndexDocument, IndexStats, Project};
use crate::analyze::{
analyze_source, build_cache, language_registry, resolve_language, walk, DEFAULT_LANG,
};
use crate::exit;
struct IndexedFile {
relative_path: String,
language: LanguageId,
result: AnalysisResult,
}
pub const DEFAULT_OUTPUT: &str = "index.scip";
#[derive(Clone, Debug)]
pub struct IndexRequest<'a> {
pub root: &'a Path,
pub output: &'a Path,
pub languages: &'a [String],
pub tool_arguments: Vec<String>,
}
#[derive(Clone, Copy, Debug)]
pub struct IndexSummary {
pub bytes: usize,
pub stats: IndexStats,
}
pub fn run(matches: &ArgMatches) -> anyhow::Result<u8> {
let output = matches
.get_one::<PathBuf>("output")
.expect("defaulted by clap");
let languages: Vec<String> = matches
.get_many::<String>("lang")
.map(|ids| ids.cloned().collect())
.unwrap_or_default();
let IndexSummary { bytes, stats } = index(IndexRequest {
root: matches
.get_one::<PathBuf>("path")
.expect("defaulted by clap"),
output,
languages: &languages,
tool_arguments: std::env::args_os()
.skip(1)
.map(|argument| argument.to_string_lossy().into_owned())
.collect(),
})?;
eprintln!(
"indexed {} files to {}: {} bytes, {} symbols, {} references \
({} same-file, {} cross-file, {} ambiguous, {} unresolved)",
stats.documents,
output.display(),
bytes,
stats.symbols,
stats.references,
stats.same_file,
stats.cross_file,
stats.ambiguous,
stats.unresolved,
);
Ok(exit::SUCCESS)
}
pub fn index(request: IndexRequest<'_>) -> anyhow::Result<IndexSummary> {
let IndexRequest {
root,
output,
languages: lang_ids,
tool_arguments,
} = request;
let lang_filter: Vec<LanguageId> = lang_ids
.iter()
.map(|id| LanguageId::from(id.as_str()))
.collect();
let languages = language_registry();
for id in &lang_filter {
if languages.get(id).is_none() {
let mut known: Vec<String> = languages.ids().map(ToString::to_string).collect();
known.sort();
anyhow::bail!(
"unknown language `{id}`; known languages: {}",
known.join(", ")
);
}
}
anyhow::ensure!(
root.is_dir(),
"project root is not a directory: {}",
root.display()
);
let absolute_root = root
.canonicalize()
.with_context(|| format!("could not resolve project root {}", root.display()))?;
let root_uri = qli_scip::file_uri(&slash_path(&absolute_root).with_context(|| {
format!(
"project root is not valid UTF-8: {}",
absolute_root.display()
)
})?);
let analyzer = DefRefs::new();
let cache = build_cache(false);
let mut files: Vec<IndexedFile> = Vec::new();
walk(root, |file| {
let Some(language) = resolve_language(file, DEFAULT_LANG, &languages) else {
tracing::debug!(path = %file.display(), "skipping: no language adapter");
return;
};
let language_id = language.id();
if !lang_filter.is_empty() && !lang_filter.contains(&language_id) {
tracing::debug!(path = %file.display(), "skipping: excluded by --lang");
return;
}
let Some(relative_path) = file.strip_prefix(root).ok().and_then(slash_path) else {
eprintln!(
"warning: skipping {}: path is not valid UTF-8",
file.display()
);
return;
};
let Some(result) = analyze_source(file, language, &analyzer, &cache) else {
return;
};
files.push(IndexedFile {
relative_path,
language: language_id,
result,
});
});
cache.enforce_size_cap();
let documents: Vec<IndexDocument<'_>> = files
.iter()
.map(|file| IndexDocument {
relative_path: &file.relative_path,
language: &file.language,
result: &file.result,
})
.collect();
let project = Project {
root_uri,
tool_version: env!("CARGO_PKG_VERSION").to_owned(),
tool_arguments,
..Default::default()
};
let (index, stats) = qli_scip::build_index(&project, &documents);
let bytes = qli_scip::encode(&index);
std::fs::write(output, &bytes)
.with_context(|| format!("could not write index to {}", output.display()))?;
Ok(IndexSummary {
bytes: bytes.len(),
stats,
})
}
fn slash_path(path: &Path) -> Option<String> {
let mut out = String::new();
for component in path.components() {
match component {
Component::RootDir => out.push('/'),
Component::CurDir => {}
other => {
if !out.is_empty() && !out.ends_with('/') {
out.push('/');
}
out.push_str(other.as_os_str().to_str()?);
}
}
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slash_path_joins_components_with_forward_slashes() {
assert_eq!(
slash_path(Path::new("a/b/c.py")).as_deref(),
Some("a/b/c.py")
);
assert_eq!(slash_path(Path::new("./a.py")).as_deref(), Some("a.py"));
assert_eq!(
slash_path(Path::new("/root/a.py")).as_deref(),
Some("/root/a.py")
);
}
}