#[cfg(any(feature = "walk", feature = "content", feature = "git"))]
use std::path::Path;
use std::path::PathBuf;
#[cfg(feature = "effort")]
use crate::effort::{EffortRequest, build_effort_report};
use anyhow::Result;
use tokmd_analysis_types::AnalysisLimits;
use tokmd_analysis_types::{
AnalysisArgsMeta, AnalysisReceipt, AnalysisSource, ApiSurfaceReport, Archetype, AssetReport,
ComplexityReport, CorporateFingerprint, DependencyReport, DuplicateReport, EntropyReport,
FunReport, GitReport, ImportReport, LicenseReport, NearDupScope, PredictiveChurnReport,
TopicClouds,
};
use tokmd_types::{ExportData, ScanStatus, ToolInfo};
#[cfg(all(feature = "content", feature = "walk"))]
use crate::api_surface::build_api_surface_report;
#[cfg(feature = "archetype")]
use crate::archetype::detect_archetype;
#[cfg(feature = "walk")]
use crate::assets::{build_assets_report, build_dependency_report};
#[cfg(feature = "content")]
use crate::content::{
ContentLimits, ImportGranularity as ContentImportGranularity, build_duplicate_report,
build_import_report, build_todo_report,
};
use crate::derived::{build_tree, derive_report};
#[cfg(feature = "git")]
use crate::fingerprint::build_corporate_fingerprint;
#[cfg(feature = "fun")]
use crate::fun::build_fun_report;
#[cfg(feature = "git")]
use crate::git::{build_git_report, build_predictive_churn_report};
use crate::grid::{PresetKind, PresetPlan, preset_plan_for};
#[cfg(feature = "content")]
use crate::near_dup::{NearDupLimits, build_near_dup_report};
#[cfg(feature = "topics")]
use crate::topics::build_topic_clouds;
use crate::util::now_ms;
#[cfg(all(feature = "content", feature = "walk"))]
use crate::{
complexity::build_complexity_report, entropy::build_entropy_report,
license::build_license_report,
};
#[cfg(all(feature = "halstead", feature = "content", feature = "walk"))]
use crate::{halstead::build_halstead_report, maintainability::attach_halstead_metrics};
pub type AnalysisPreset = PresetKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportGranularity {
Module,
File,
}
#[cfg(feature = "content")]
fn content_limits(limits: &AnalysisLimits) -> ContentLimits {
ContentLimits {
max_bytes: limits.max_bytes,
max_file_bytes: limits.max_file_bytes,
}
}
#[cfg(feature = "content")]
fn content_import_granularity(granularity: ImportGranularity) -> ContentImportGranularity {
match granularity {
ImportGranularity::Module => ContentImportGranularity::Module,
ImportGranularity::File => ContentImportGranularity::File,
}
}
#[derive(Debug, Clone)]
pub struct AnalysisContext {
pub export: ExportData,
pub root: PathBuf,
pub source: AnalysisSource,
}
#[derive(Debug, Clone)]
pub struct AnalysisRequest {
pub preset: AnalysisPreset,
pub args: AnalysisArgsMeta,
pub limits: AnalysisLimits,
#[cfg(feature = "effort")]
pub effort: Option<EffortRequest>,
pub window_tokens: Option<usize>,
pub git: Option<bool>,
pub import_granularity: ImportGranularity,
pub detail_functions: bool,
pub near_dup: bool,
pub near_dup_threshold: f64,
pub near_dup_max_files: usize,
pub near_dup_scope: NearDupScope,
pub near_dup_max_pairs: Option<usize>,
pub near_dup_exclude: Vec<String>,
}
fn preset_plan(preset: AnalysisPreset) -> PresetPlan {
preset_plan_for(preset)
}
#[cfg(any(feature = "walk", feature = "content", feature = "git"))]
const ROOTLESS_FILE_ANALYSIS_WARNING: &str =
"in-memory analysis has no host root; skipping file-backed enrichers";
#[cfg(feature = "git")]
const ROOTLESS_GIT_ANALYSIS_WARNING: &str =
"in-memory analysis has no host root; skipping git-backed enrichers";
#[cfg(any(feature = "walk", feature = "content", feature = "git"))]
fn has_host_root(root: &Path) -> bool {
!root.as_os_str().is_empty()
}
#[cfg(any(feature = "walk", feature = "content", feature = "git"))]
fn push_warning_once(warnings: &mut Vec<String>, warning: &str) {
if warnings.iter().all(|existing| existing != warning) {
warnings.push(warning.to_string());
}
}
pub fn analyze(ctx: AnalysisContext, req: AnalysisRequest) -> Result<AnalysisReceipt> {
let mut warnings: Vec<String> = Vec::new();
#[cfg_attr(not(feature = "content"), allow(unused_mut))]
let mut derived = derive_report(&ctx.export, req.window_tokens);
if req.args.format.contains("tree") {
derived.tree = Some(build_tree(&ctx.export));
}
let mut source = ctx.source.clone();
if source.base_signature.is_none() {
source.base_signature = Some(derived.integrity.hash.clone());
}
let plan = preset_plan(req.preset);
let include_git = match req.git {
Some(flag) => flag,
None => plan.git,
};
#[cfg(any(feature = "walk", feature = "content", feature = "git"))]
let has_host_root = has_host_root(&ctx.root);
#[cfg(feature = "walk")]
let mut assets: Option<AssetReport> = None;
#[cfg(not(feature = "walk"))]
let assets: Option<AssetReport> = None;
#[cfg(feature = "walk")]
let mut deps: Option<DependencyReport> = None;
#[cfg(not(feature = "walk"))]
let deps: Option<DependencyReport> = None;
#[cfg(feature = "content")]
let mut imports: Option<ImportReport> = None;
#[cfg(not(feature = "content"))]
let imports: Option<ImportReport> = None;
#[cfg(feature = "content")]
let mut dup: Option<DuplicateReport> = None;
#[cfg(not(feature = "content"))]
let dup: Option<DuplicateReport> = None;
#[cfg(feature = "git")]
let mut git: Option<GitReport> = None;
#[cfg(not(feature = "git"))]
let git: Option<GitReport> = None;
#[cfg(feature = "git")]
let mut churn: Option<PredictiveChurnReport> = None;
#[cfg(not(feature = "git"))]
let churn: Option<PredictiveChurnReport> = None;
#[cfg(feature = "git")]
let mut fingerprint: Option<CorporateFingerprint> = None;
#[cfg(not(feature = "git"))]
let fingerprint: Option<CorporateFingerprint> = None;
#[cfg(all(feature = "content", feature = "walk"))]
let mut entropy: Option<EntropyReport> = None;
#[cfg(not(all(feature = "content", feature = "walk")))]
let entropy: Option<EntropyReport> = None;
#[cfg(all(feature = "content", feature = "walk"))]
let mut license: Option<LicenseReport> = None;
#[cfg(not(all(feature = "content", feature = "walk")))]
let license: Option<LicenseReport> = None;
#[cfg(all(feature = "content", feature = "walk"))]
let mut complexity: Option<ComplexityReport> = None;
#[cfg(not(all(feature = "content", feature = "walk")))]
let complexity: Option<ComplexityReport> = None;
#[cfg(all(feature = "content", feature = "walk"))]
let mut api_surface: Option<ApiSurfaceReport> = None;
#[cfg(not(all(feature = "content", feature = "walk")))]
let api_surface: Option<ApiSurfaceReport> = None;
#[cfg(feature = "archetype")]
let mut archetype: Option<Archetype> = None;
#[cfg(not(feature = "archetype"))]
let archetype: Option<Archetype> = None;
#[cfg(feature = "topics")]
let mut topics: Option<TopicClouds> = None;
#[cfg(not(feature = "topics"))]
let topics: Option<TopicClouds> = None;
let fun: Option<FunReport>;
#[cfg(any(feature = "walk", feature = "content"))]
#[cfg_attr(not(feature = "walk"), allow(unused_mut))]
let mut files: Option<Vec<PathBuf>> = None;
#[cfg(not(any(feature = "walk", feature = "content")))]
let _files: Option<Vec<PathBuf>> = None;
if plan.needs_files() {
#[cfg(feature = "walk")]
if has_host_root {
match tokmd_scan::walk::list_files(&ctx.root, req.limits.max_files) {
Ok(list) => files = Some(list),
Err(err) => warnings.push(format!("walk failed: {}", err)),
}
} else {
push_warning_once(&mut warnings, ROOTLESS_FILE_ANALYSIS_WARNING);
}
#[cfg(not(feature = "walk"))]
{
warnings.push(
crate::grid::DisabledFeature::FileInventory
.warning()
.to_string(),
);
}
}
if plan.assets {
#[cfg(feature = "walk")]
{
if let Some(list) = files.as_deref() {
match build_assets_report(&ctx.root, list) {
Ok(report) => assets = Some(report),
Err(err) => warnings.push(format!("asset scan failed: {}", err)),
}
}
}
}
if plan.deps {
#[cfg(feature = "walk")]
{
if let Some(list) = files.as_deref() {
match build_dependency_report(&ctx.root, list) {
Ok(report) => deps = Some(report),
Err(err) => warnings.push(format!("dependency scan failed: {}", err)),
}
}
}
}
if plan.todo {
#[cfg(feature = "content")]
{
if let Some(list) = files.as_deref() {
let limits = content_limits(&req.limits);
match build_todo_report(&ctx.root, list, &limits, derived.totals.code) {
Ok(report) => derived.todo = Some(report),
Err(err) => warnings.push(format!("todo scan failed: {}", err)),
}
}
}
#[cfg(not(feature = "content"))]
warnings.push(crate::grid::DisabledFeature::TodoScan.warning().to_string());
}
if plan.dup {
#[cfg(feature = "content")]
{
if let Some(list) = files.as_deref() {
let limits = content_limits(&req.limits);
match build_duplicate_report(&ctx.root, list, &ctx.export, &limits) {
Ok(report) => dup = Some(report),
Err(err) => warnings.push(format!("dup scan failed: {}", err)),
}
}
}
#[cfg(not(feature = "content"))]
warnings.push(
crate::grid::DisabledFeature::DuplicationScan
.warning()
.to_string(),
);
}
if req.near_dup {
#[cfg(feature = "content")]
{
if has_host_root {
let near_dup_limits = NearDupLimits {
max_bytes: req.limits.max_bytes,
max_file_bytes: req.limits.max_file_bytes,
};
match build_near_dup_report(
&ctx.root,
&ctx.export,
req.near_dup_scope,
req.near_dup_threshold,
req.near_dup_max_files,
req.near_dup_max_pairs,
&near_dup_limits,
&req.near_dup_exclude,
) {
Ok(report) => {
if let Some(ref mut d) = dup {
d.near = Some(report);
} else {
dup = Some(DuplicateReport {
groups: Vec::new(),
wasted_bytes: 0,
strategy: "none".to_string(),
density: None,
near: Some(report),
});
}
}
Err(err) => warnings.push(format!("near-dup scan failed: {}", err)),
}
} else {
push_warning_once(&mut warnings, ROOTLESS_FILE_ANALYSIS_WARNING);
}
}
#[cfg(not(feature = "content"))]
warnings.push(
crate::grid::DisabledFeature::NearDuplicateScan
.warning()
.to_string(),
);
}
if plan.imports {
#[cfg(feature = "content")]
{
if let Some(list) = files.as_deref() {
let limits = content_limits(&req.limits);
let granularity = content_import_granularity(req.import_granularity);
match build_import_report(&ctx.root, list, &ctx.export, granularity, &limits) {
Ok(report) => imports = Some(report),
Err(err) => warnings.push(format!("import scan failed: {}", err)),
}
}
}
#[cfg(not(feature = "content"))]
warnings.push(
crate::grid::DisabledFeature::ImportScan
.warning()
.to_string(),
);
}
if include_git {
#[cfg(feature = "git")]
{
if has_host_root {
let repo_root = match tokmd_git::repo_root(&ctx.root) {
Some(root) => root,
None => {
warnings.push("git scan failed: not a git repo".to_string());
PathBuf::new()
}
};
if !repo_root.as_os_str().is_empty() {
match tokmd_git::collect_history(
&repo_root,
req.limits.max_commits,
req.limits.max_commit_files,
) {
Ok(commits) => {
if plan.git {
match build_git_report(&repo_root, &ctx.export, &commits) {
Ok(report) => git = Some(report),
Err(err) => warnings.push(format!("git scan failed: {}", err)),
}
}
if plan.churn {
churn = Some(build_predictive_churn_report(
&ctx.export,
&commits,
&repo_root,
));
}
if plan.fingerprint {
fingerprint = Some(build_corporate_fingerprint(&commits));
}
}
Err(err) => warnings.push(format!("git scan failed: {}", err)),
}
}
} else {
push_warning_once(&mut warnings, ROOTLESS_GIT_ANALYSIS_WARNING);
}
}
#[cfg(not(feature = "git"))]
warnings.push(
crate::grid::DisabledFeature::GitMetrics
.warning()
.to_string(),
);
}
if plan.archetype {
#[cfg(feature = "archetype")]
{
archetype = detect_archetype(&ctx.export);
}
#[cfg(not(feature = "archetype"))]
{
warnings.push(
crate::grid::DisabledFeature::Archetype
.warning()
.to_string(),
);
}
}
if plan.topics {
#[cfg(feature = "topics")]
{
topics = Some(build_topic_clouds(&ctx.export));
}
#[cfg(not(feature = "topics"))]
{
warnings.push(crate::grid::DisabledFeature::Topics.warning().to_string());
}
}
if plan.entropy {
#[cfg(all(feature = "content", feature = "walk"))]
{
if let Some(list) = files.as_deref() {
match build_entropy_report(&ctx.root, list, &ctx.export, &req.limits) {
Ok(report) => entropy = Some(report),
Err(err) => warnings.push(format!("entropy scan failed: {}", err)),
}
}
}
#[cfg(not(all(feature = "content", feature = "walk")))]
warnings.push(
crate::grid::DisabledFeature::EntropyProfiling
.warning()
.to_string(),
);
}
if plan.license {
#[cfg(all(feature = "content", feature = "walk"))]
{
if let Some(list) = files.as_deref() {
match build_license_report(&ctx.root, list, &req.limits) {
Ok(report) => license = Some(report),
Err(err) => warnings.push(format!("license scan failed: {}", err)),
}
}
}
#[cfg(not(all(feature = "content", feature = "walk")))]
warnings.push(
crate::grid::DisabledFeature::LicenseRadar
.warning()
.to_string(),
);
}
if plan.complexity {
#[cfg(all(feature = "content", feature = "walk"))]
{
if let Some(list) = files.as_deref() {
match build_complexity_report(
&ctx.root,
list,
&ctx.export,
&req.limits,
req.detail_functions,
) {
Ok(report) => complexity = Some(report),
Err(err) => warnings.push(format!("complexity scan failed: {}", err)),
}
}
}
#[cfg(not(all(feature = "content", feature = "walk")))]
warnings.push(
crate::grid::DisabledFeature::ComplexityAnalysis
.warning()
.to_string(),
);
}
if plan.api_surface {
#[cfg(all(feature = "content", feature = "walk"))]
{
if let Some(list) = files.as_deref() {
match build_api_surface_report(&ctx.root, list, &ctx.export, &req.limits) {
Ok(report) => api_surface = Some(report),
Err(err) => warnings.push(format!("api surface scan failed: {}", err)),
}
}
}
#[cfg(not(all(feature = "content", feature = "walk")))]
warnings.push(
crate::grid::DisabledFeature::ApiSurfaceAnalysis
.warning()
.to_string(),
);
}
#[cfg(all(feature = "halstead", feature = "content", feature = "walk"))]
if plan.halstead
&& let Some(list) = files.as_deref()
{
match build_halstead_report(&ctx.root, list, &ctx.export, &req.limits) {
Ok(halstead_report) => {
if let Some(ref mut cx) = complexity {
attach_halstead_metrics(cx, halstead_report);
}
}
Err(err) => warnings.push(format!("halstead scan failed: {}", err)),
}
}
if plan.fun {
#[cfg(feature = "fun")]
{
fun = Some(build_fun_report(&derived));
}
#[cfg(not(feature = "fun"))]
{
warnings.push(crate::grid::DisabledFeature::Fun.warning().to_string());
fun = None;
}
} else {
fun = None;
}
#[cfg(feature = "effort")]
let effort = if let Some(effort_request) = &req.effort {
match build_effort_report(
&ctx.root,
&ctx.export,
&derived,
git.as_ref(),
complexity.as_ref(),
api_surface.as_ref(),
dup.as_ref(),
effort_request,
) {
Ok(report) => Some(report),
Err(err) => {
warnings.push(format!("effort estimate failed: {}", err));
None
}
}
} else {
None
};
#[cfg(not(feature = "effort"))]
let effort: Option<tokmd_analysis_types::EffortEstimateReport> = None;
let status = if warnings.is_empty() {
ScanStatus::Complete
} else {
ScanStatus::Partial
};
let receipt = AnalysisReceipt {
schema_version: tokmd_analysis_types::ANALYSIS_SCHEMA_VERSION,
generated_at_ms: now_ms(),
tool: ToolInfo::current(),
mode: "analysis".to_string(),
status,
warnings,
source,
args: req.args,
archetype,
topics,
entropy,
predictive_churn: churn,
corporate_fingerprint: fingerprint,
license,
derived: Some(derived),
assets,
deps,
git,
imports,
dup,
complexity,
api_surface,
effort,
fun,
};
Ok(receipt)
}