use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
pub const DEFAULT_LOC_THRESHOLD: usize = 1000;
pub const COLOR_RED: &str = "\u{001b}[31m";
pub const COLOR_RESET: &str = "\u{001b}[0m";
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum ColorMode {
#[default]
Auto,
Always,
Never,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum OutputMode {
Human,
Json,
Jsonl,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Mode {
Tree,
AnalyzeImports,
Init,
Slice,
Trace,
ForAi,
Findings,
Summary,
Git(GitSubcommand),
Search,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum GitSubcommand {
Compare {
from: String,
to: Option<String>,
},
Blame {
file: String,
},
History {
symbol: Option<String>,
file: Option<String>,
limit: usize,
},
WhenIntroduced {
circular: Option<String>,
dead: Option<String>,
import: Option<String>,
},
}
#[derive(Clone)]
pub struct Options {
pub extensions: Option<HashSet<String>>,
pub ignore_paths: Vec<std::path::PathBuf>,
pub ignore_globs: Option<std::sync::Arc<globset::GlobSet>>,
pub use_gitignore: bool,
pub max_depth: Option<usize>,
pub color: ColorMode,
pub output: OutputMode,
pub summary: bool,
pub summary_limit: usize,
pub summary_only: bool,
pub show_hidden: bool,
pub show_ignored: bool,
pub loc_threshold: usize,
pub analyze_limit: usize,
pub report_path: Option<std::path::PathBuf>,
pub serve: bool,
pub editor_cmd: Option<String>,
pub max_graph_nodes: Option<usize>,
pub max_graph_edges: Option<usize>,
pub verbose: bool,
pub scan_all: bool,
pub symbol: Option<String>,
pub impact: Option<String>,
pub find_artifacts: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
extensions: None,
ignore_paths: Vec::new(),
ignore_globs: None,
use_gitignore: true,
max_depth: None,
color: ColorMode::Auto,
output: OutputMode::Human,
summary: false,
summary_limit: 50,
summary_only: false,
show_hidden: false,
show_ignored: false,
loc_threshold: 500,
analyze_limit: 100,
report_path: None,
serve: false,
editor_cmd: None,
max_graph_nodes: None,
max_graph_edges: None,
verbose: false,
scan_all: false,
symbol: None,
impact: None,
find_artifacts: false,
}
}
}
pub struct LineEntry {
pub label: String,
pub loc: Option<usize>,
pub relative_path: String,
pub is_dir: bool,
pub is_large: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SymbolMatch {
pub line: usize,
pub context: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Language {
Rust,
Python,
Typescript,
Javascript,
Shell,
Makefile,
Css,
Html,
Go,
Dart,
Zig,
Other(String),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LocalSymbol {
pub name: String,
pub kind: String,
#[serde(default)]
pub line: Option<usize>,
#[serde(default)]
pub context: String,
#[serde(default)]
pub is_exported: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SymbolUsage {
pub name: String,
pub line: usize,
#[serde(default)]
pub context: String,
}
pub struct LargeEntry {
pub path: String,
pub loc: usize,
}
#[derive(Default)]
pub struct Stats {
pub directories: usize,
pub files: usize,
pub files_with_loc: usize,
pub total_loc: usize,
}
pub struct Collectors<'a> {
pub entries: &'a mut Vec<LineEntry>,
pub large_entries: &'a mut Vec<LargeEntry>,
pub stats: &'a mut Stats,
}
pub use loctree_ast::CallEntry;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ImportEntry {
#[serde(default)]
pub line: Option<usize>,
pub source: String,
pub source_raw: String,
pub kind: ImportKind,
pub resolved_path: Option<String>,
pub is_bare: bool,
pub symbols: Vec<ImportSymbol>,
pub resolution: ImportResolutionKind,
pub is_type_checking: bool,
#[serde(default)]
pub is_lazy: bool,
#[serde(default)]
pub is_crate_relative: bool,
#[serde(default)]
pub is_super_relative: bool,
#[serde(default)]
pub is_self_relative: bool,
#[serde(default)]
pub is_mod_declaration: bool,
#[serde(default)]
pub raw_path: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ImportKind {
Static,
Type,
SideEffect,
Dynamic,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ImportResolutionKind {
Local,
Stdlib,
Dynamic,
Unknown,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ImportSymbol {
pub name: String,
pub alias: Option<String>,
#[serde(default)]
pub is_default: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ReexportEntry {
pub source: String,
pub kind: ReexportKind,
pub resolved: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ReexportKind {
Star,
Named(Vec<(String, String)>),
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ParamInfo {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_annotation: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub has_default: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ExportSymbol {
pub name: String,
pub kind: String,
pub export_type: String,
pub line: Option<usize>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub params: Vec<ParamInfo>,
#[serde(default, skip_serializing_if = "SymbolIdV1::is_empty")]
pub symbol_id: SymbolIdV1,
}
#[derive(
Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema,
)]
pub struct SymbolIdV1(pub String);
impl SymbolIdV1 {
pub const VERSION: &'static str = "v1-string";
pub fn from_parts(file: &str, symbol: &str) -> Self {
Self(format!("{}::{}", file, symbol))
}
pub fn from_export(file: &str, export: &ExportSymbol) -> Self {
Self::from_parts(file, &export.name)
}
pub fn split_parts(&self) -> Option<(&str, &str)> {
self.0.split_once("::")
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn version() -> &'static str {
Self::VERSION
}
}
#[derive(
Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema,
)]
pub struct SymbolIdV2(pub String);
impl SymbolIdV2 {
pub const VERSION: &'static str = "v2-byte-range";
pub fn from_parts(file: &str, kind: &str, name: &str, byte_range: (usize, usize)) -> Self {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
byte_range.0.hash(&mut hasher);
byte_range.1.hash(&mut hasher);
let hash = hasher.finish();
Self(format!("{}::{}::{}::{:016x}", file, kind, name, hash))
}
pub fn to_v1(&self) -> SymbolIdV1 {
let parts: Vec<&str> = self.0.splitn(4, "::").collect();
if parts.len() == 4 {
SymbolIdV1::from_parts(parts[0], parts[2])
} else {
SymbolIdV1(self.0.clone())
}
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn version() -> &'static str {
Self::VERSION
}
}
impl std::fmt::Display for SymbolIdV2 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::fmt::Display for SymbolIdV1 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for SymbolIdV1 {
fn from(value: String) -> Self {
Self(value)
}
}
impl From<&str> for SymbolIdV1 {
fn from(value: &str) -> Self {
Self(value.to_string())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CommandRef {
pub name: String,
pub exposed_name: Option<String>,
pub line: usize,
pub generic_type: Option<String>,
pub payload: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub plugin_name: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CommandPayloadCasing {
pub command: String,
pub key: String,
pub path: String,
pub line: usize,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct StringLiteral {
pub value: String,
pub line: usize,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct RouteInfo {
pub framework: String,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub line: usize,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct DynamicExecTemplate {
pub template: String,
pub generated_prefixes: Vec<String>,
pub line: usize,
pub call_type: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SysModulesInjection {
pub module_name: String,
pub line: usize,
pub value: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EventRef {
pub raw_name: Option<String>,
pub name: String,
pub line: usize,
pub kind: String,
pub awaited: bool,
pub payload: Option<String>,
#[serde(default)]
pub is_dynamic: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PyRaceIndicator {
pub line: usize,
pub concurrency_type: String,
pub pattern: String,
pub risk: String,
pub message: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Visibility {
Public,
Crate,
Restricted(String),
#[default]
Private,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ImplMethod {
pub name: String,
pub qualifier: String,
#[serde(default)]
pub trait_qualifier: Option<String>,
#[serde(default)]
pub line: Option<usize>,
#[serde(default)]
pub is_async: bool,
#[serde(default)]
pub visibility: Visibility,
#[serde(default = "default_true")]
pub is_definition: bool,
}
fn default_true() -> bool {
true
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TargetKind {
Bin,
Lib,
Example,
Bench,
Test,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CargoTarget {
pub name: String,
pub kind: TargetKind,
pub path: PathBuf,
pub crate_root: PathBuf,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
Panic,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LogMessage {
pub level: LogLevel,
pub macro_or_fn: String,
pub format_string: String,
pub line: usize,
#[serde(default)]
pub function_context: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct FileAnalysis {
#[serde(default)]
pub path: String,
#[serde(default)]
pub loc: usize,
#[serde(default)]
pub language: String,
#[serde(default)]
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resource_kind: Option<String>,
#[serde(default)]
pub is_test: bool,
#[serde(default)]
pub is_generated: bool,
#[serde(default)]
pub is_flow_file: bool,
#[serde(default)]
pub imports: Vec<ImportEntry>,
#[serde(default)]
pub reexports: Vec<ReexportEntry>,
#[serde(default)]
pub dynamic_imports: Vec<String>,
#[serde(default)]
pub exports: Vec<ExportSymbol>,
#[serde(default)]
pub local_symbols: Vec<LocalSymbol>,
#[serde(default)]
pub symbol_usages: Vec<SymbolUsage>,
#[serde(default)]
pub command_calls: Vec<CommandRef>,
#[serde(default)]
pub command_handlers: Vec<CommandRef>,
#[serde(default)]
pub command_payload_casing: Vec<CommandPayloadCasing>,
#[serde(default)]
pub string_literals: Vec<StringLiteral>,
#[serde(default)]
pub event_emits: Vec<EventRef>,
#[serde(default)]
pub event_listens: Vec<EventRef>,
#[serde(default)]
pub event_consts: HashMap<String, String>,
#[serde(default)]
pub matches: Vec<SymbolMatch>,
#[serde(default)]
pub entry_points: Vec<String>,
#[serde(default)]
pub tauri_registered_handlers: Vec<String>,
#[serde(default)]
pub mtime: u64,
#[serde(default)]
pub size: u64,
#[serde(default)]
pub py_race_indicators: Vec<PyRaceIndicator>,
#[serde(default)]
pub is_typed_package: bool,
#[serde(default)]
pub is_namespace_package: bool,
#[serde(default)]
pub local_uses: Vec<String>,
#[serde(default)]
pub signature_uses: Vec<SignatureUse>,
#[serde(default)]
pub routes: Vec<RouteInfo>,
#[serde(default)]
pub pytest_fixtures: Vec<String>,
#[serde(default)]
pub has_weak_collections: bool,
#[serde(default)]
pub dynamic_exec_templates: Vec<DynamicExecTemplate>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sys_modules_injections: Vec<SysModulesInjection>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub semantic_facts_ref: Option<SemanticFactsRef>,
#[serde(default)]
pub impl_methods: Vec<ImplMethod>,
#[serde(default)]
pub cargo_targets: Vec<CargoTarget>,
#[serde(default)]
pub log_messages: Vec<LogMessage>,
#[serde(default)]
pub crate_membership: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol_fragment: Option<crate::symbols::SymbolGraph>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SemanticFactsRef {
pub idiom_tag_count: u32,
pub has_dispatch_edges: bool,
pub has_env_contracts: bool,
}
impl ImportEntry {
pub fn new(source: String, kind: ImportKind) -> Self {
let is_bare = !source.starts_with('.') && !source.starts_with('/');
Self {
line: None,
source_raw: source.clone(),
source,
kind,
resolved_path: None,
is_bare,
symbols: Vec::new(),
resolution: ImportResolutionKind::Unknown,
is_type_checking: false,
is_lazy: false,
is_crate_relative: false,
is_super_relative: false,
is_self_relative: false,
raw_path: String::new(),
is_mod_declaration: false,
}
}
}
impl ExportSymbol {
pub fn new(name: String, kind: &str, export_type: &str, line: Option<usize>) -> Self {
Self {
name,
kind: kind.to_string(),
export_type: export_type.to_string(),
line,
params: Vec::new(),
symbol_id: SymbolIdV1::default(),
}
}
pub fn with_params(
name: String,
kind: &str,
export_type: &str,
line: Option<usize>,
params: Vec<ParamInfo>,
) -> Self {
Self {
name,
kind: kind.to_string(),
export_type: export_type.to_string(),
line,
params,
symbol_id: SymbolIdV1::default(),
}
}
pub fn with_symbol_id(mut self, file: &str) -> Self {
self.symbol_id = SymbolIdV1::from_parts(file, &self.name);
self
}
}
impl FileAnalysis {
pub fn new(path: String) -> Self {
Self {
path,
loc: 0,
language: String::new(),
kind: "code".to_string(),
resource_kind: None,
is_test: false,
is_generated: false,
is_flow_file: false,
imports: Vec::new(),
reexports: Vec::new(),
dynamic_imports: Vec::new(),
exports: Vec::new(),
local_symbols: Vec::new(),
symbol_usages: Vec::new(),
command_calls: Vec::new(),
command_handlers: Vec::new(),
command_payload_casing: Vec::new(),
string_literals: Vec::new(),
event_emits: Vec::new(),
event_listens: Vec::new(),
event_consts: HashMap::new(),
matches: Vec::new(),
entry_points: Vec::new(),
tauri_registered_handlers: Vec::new(),
py_race_indicators: Vec::new(),
mtime: 0,
size: 0,
is_typed_package: false,
is_namespace_package: false,
local_uses: Vec::new(),
signature_uses: Vec::new(),
routes: Vec::new(),
pytest_fixtures: Vec::new(),
has_weak_collections: false,
dynamic_exec_templates: Vec::new(),
sys_modules_injections: Vec::new(),
semantic_facts_ref: None,
impl_methods: Vec::new(),
cargo_targets: Vec::new(),
log_messages: Vec::new(),
crate_membership: None,
symbol_fragment: None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SignatureUseKind {
Parameter,
Return,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SignatureUse {
pub function: String,
pub usage: SignatureUseKind,
pub type_name: String,
#[serde(default)]
pub line: Option<usize>,
}
pub type ExportIndex = HashMap<String, Vec<String>>;
pub type PayloadEntry = (String, usize, Option<String>);
pub type PayloadMap = HashMap<String, Vec<PayloadEntry>>;
#[cfg(test)]
mod symbol_id_v1_tests {
use super::{ExportSymbol, SymbolIdV1};
fn export(name: &str) -> ExportSymbol {
ExportSymbol {
name: name.to_string(),
kind: "function".to_string(),
export_type: "named".to_string(),
line: Some(42),
params: Vec::new(),
symbol_id: SymbolIdV1::default(),
}
}
#[test]
fn from_parts_uses_double_colon_separator() {
let id = SymbolIdV1::from_parts("src/lib.rs", "compose_runtime_slice");
assert_eq!(id.as_str(), "src/lib.rs::compose_runtime_slice");
}
#[test]
fn from_export_matches_legacy_string_alias_format() {
let id = SymbolIdV1::from_export("src/lib.rs", &export("foo"));
assert_eq!(id.as_str(), "src/lib.rs::foo");
}
#[test]
fn split_parts_recovers_file_and_symbol() {
let id = SymbolIdV1::from_parts("a/b.rs", "Bar");
assert_eq!(id.split_parts(), Some(("a/b.rs", "Bar")));
}
#[test]
fn split_parts_returns_none_for_malformed_id() {
let id = SymbolIdV1("bare-symbol".into());
assert!(id.split_parts().is_none());
}
#[test]
fn version_is_v1_string() {
assert_eq!(SymbolIdV1::version(), "v1-string");
}
#[test]
fn serializes_as_plain_string_for_wire_compat() {
let id = SymbolIdV1::from_parts("f.rs", "g");
let json = serde_json::to_value(&id).unwrap();
assert_eq!(json, serde_json::json!("f.rs::g"));
}
#[test]
fn round_trips_through_serde() {
let id = SymbolIdV1::from_parts("f.rs", "g");
let json = serde_json::to_string(&id).unwrap();
let back: SymbolIdV1 = serde_json::from_str(&json).unwrap();
assert_eq!(id, back);
}
#[test]
fn default_is_empty_so_skip_serializing_kicks_in() {
let id = SymbolIdV1::default();
assert!(id.is_empty());
assert_eq!(id.as_str(), "");
}
#[test]
fn export_with_symbol_id_populates_v1_form() {
let exp = ExportSymbol::new("foo".to_string(), "function", "named", Some(1))
.with_symbol_id("src/lib.rs");
assert_eq!(exp.symbol_id.as_str(), "src/lib.rs::foo");
}
#[test]
fn export_default_symbol_id_serializes_without_field_for_back_compat() {
let exp = ExportSymbol::new("foo".to_string(), "function", "named", Some(1));
let json = serde_json::to_value(&exp).unwrap();
assert!(json.get("symbol_id").is_none(), "got: {json}");
}
#[test]
fn export_with_symbol_id_serializes_field() {
let exp = ExportSymbol::new("foo".to_string(), "function", "named", Some(1))
.with_symbol_id("src/lib.rs");
let json = serde_json::to_value(&exp).unwrap();
assert_eq!(json["symbol_id"], serde_json::json!("src/lib.rs::foo"));
}
}
#[cfg(test)]
mod symbol_id_v2_tests {
use super::SymbolIdV2;
#[test]
fn from_parts_includes_file_kind_name_and_hash() {
let id = SymbolIdV2::from_parts("src/lib.rs", "function", "greet", (16, 21));
let parts: Vec<&str> = id.as_str().splitn(4, "::").collect();
assert_eq!(parts[0], "src/lib.rs");
assert_eq!(parts[1], "function");
assert_eq!(parts[2], "greet");
assert_eq!(parts[3].len(), 16, "hash component must be 16 hex chars");
}
#[test]
fn equal_ranges_produce_equal_hash() {
let a = SymbolIdV2::from_parts("a.ts", "function", "f", (10, 20));
let b = SymbolIdV2::from_parts("a.ts", "function", "f", (10, 20));
assert_eq!(a, b);
}
#[test]
fn different_ranges_produce_different_hash() {
let a = SymbolIdV2::from_parts("a.ts", "function", "f", (10, 20));
let b = SymbolIdV2::from_parts("a.ts", "function", "f", (10, 21));
assert_ne!(a, b, "hash must roll when byte range changes");
}
#[test]
fn to_v1_recovers_file_and_name() {
let id = SymbolIdV2::from_parts("src/lib.rs", "function", "greet", (16, 21));
let v1 = id.to_v1();
assert_eq!(v1.as_str(), "src/lib.rs::greet");
}
#[test]
fn version_is_v2_byte_range() {
assert_eq!(SymbolIdV2::version(), "v2-byte-range");
}
#[test]
fn serializes_as_plain_string_for_wire_compat() {
let id = SymbolIdV2::from_parts("f.rs", "class", "C", (0, 100));
let json = serde_json::to_value(&id).unwrap();
assert!(json.is_string(), "v2 ids serialize as bare strings");
}
}