refactor(pi-natives): restructured config handling with env_uint! macro and scope validation

- Extracted environment variable parsing into centralized `env_uint!` macro for consistent configuration handling across modules.
- Refactored recursion logic to validate scope narrowing before recursing, introducing `recursion_narrows_scope()` and `MIN_RECURSE_SAVINGS` constant.
- Consolidated static configuration initialization from LazyLock to declarative `env_uint!` macro in render and cache modules.
- Improved control flow statement classification in defaults module by replacing positional candidates with named function-context candidates.
This commit is contained in:
can1357
2026-04-06 18:48:41 +02:00
parent b93d1929db
commit 49560bdc9b
6 changed files with 161 additions and 80 deletions
+8 -15
View File
@@ -4,28 +4,21 @@
//! [`RawChunkCandidate`] values. They are also used by the default (shared)
//! classification in [`super::defaults`].
use std::sync::LazyLock;
use tree_sitter::Node;
use super::types::ChunkNode;
use crate::env_uint;
// ── Configuration (environment overrides) ────────────────────────────────
//
// - `PI_CHUNK_LEAF_THRESHOLD` – default `15`
// - `PI_CHUNK_MAX_LINES` – default `25`
fn env_usize(name: &str, default: usize) -> usize {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
env_uint! {
// Configured leaf threshold.
pub static LEAF_THRESHOLD: usize = "PI_CHUNK_LEAF_THRESHOLD" or 15 => [1, usize::MAX];
// Configured max chunk lines.
pub static MAX_CHUNK_LINES: usize = "PI_CHUNK_MAX_LINES" or 25 => [1, usize::MAX];
// Configured min recurse savings.
pub static MIN_RECURSE_SAVINGS: usize = "PI_CHUNK_MIN_SAVINGS" or 5 => [1, usize::MAX];
}
pub static LEAF_THRESHOLD: LazyLock<usize> =
LazyLock::new(|| env_usize("PI_CHUNK_LEAF_THRESHOLD", 15));
pub static MAX_CHUNK_LINES: LazyLock<usize> = LazyLock::new(|| env_usize("PI_CHUNK_MAX_LINES", 25));
// ── Internal types ───────────────────────────────────────────────────────
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+70 -15
View File
@@ -48,6 +48,26 @@ pub fn classify_root_default<'tree>(node: Node<'tree>, source: &str) -> RawChunk
group_candidate(node, "stmts", source)
},
// ── Control flow (top-level scripts) ──
"if_statement"
| "unless"
| "guard_statement"
| "switch_statement"
| "switch_expression"
| "case_statement"
| "expression_switch_statement"
| "type_switch_statement"
| "select_statement"
| "try_statement"
| "try_block"
| "for_statement"
| "for_in_statement"
| "for_of_statement"
| "foreach_statement"
| "while_statement"
| "do_statement"
| "with_statement" => classify_function_default(node, source),
// ── Containers / namespaces / modules ──
"class_declaration"
| "class_definition"
@@ -241,8 +261,11 @@ pub fn classify_function_default<'tree>(
node: Node<'tree>,
source: &str,
) -> RawChunkCandidate<'tree> {
let fn_recurse = || recurse_body(node, ChunkContext::FunctionBody);
match node.kind() {
"if_statement" | "unless" | "guard_statement" => positional_candidate(node, "if", source),
"if_statement" | "unless" | "guard_statement" => {
make_candidate(node, "if".to_string(), NameStyle::Named, None, fn_recurse(), false, source)
},
"switch_statement"
| "switch_expression"
| "case_statement"
@@ -251,29 +274,61 @@ pub fn classify_function_default<'tree>(
| "type_switch_statement"
| "select_statement"
| "receive_statement"
| "yul_switch_statement" => positional_candidate(node, "switch", source),
| "yul_switch_statement" => make_candidate(
node,
"switch".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"try_statement" | "try_block" | "catch_clause" | "finally_clause" | "assembly_statement" => {
positional_candidate(node, "try", source)
make_candidate(
node,
"try".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
)
},
"for_statement" | "for_in_statement" | "for_of_statement" => {
if looks_like_python_statement(node, source) {
positional_candidate(node, "loop", source)
let name = if looks_like_python_statement(node, source) {
"loop".to_string()
} else {
positional_candidate(node, "for", source)
}
sanitize_node_kind(node.kind())
};
make_candidate(node, name, NameStyle::Named, None, fn_recurse(), false, source)
},
"while_statement" => {
if looks_like_python_statement(node, source) {
positional_candidate(node, "loop", source)
let name = if looks_like_python_statement(node, source) {
"loop"
} else {
positional_candidate(node, "while", source)
}
"while"
};
make_candidate(node, name.to_string(), NameStyle::Named, None, fn_recurse(), false, source)
},
"do_statement" | "with_statement" | "do_block" | "subshell" | "async_block"
| "unsafe_block" | "const_block" | "block_expression" => {
positional_candidate(node, "block", source)
},
"foreach_statement" => positional_candidate(node, "for", source),
| "unsafe_block" | "const_block" | "block_expression" => make_candidate(
node,
"block".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"foreach_statement" => make_candidate(
node,
"for".to_string(),
NameStyle::Named,
None,
fn_recurse(),
false,
source,
),
"defer_statement" | "go_statement" | "send_statement" => {
group_candidate(node, "stmts", source)
},
+20 -1
View File
@@ -299,7 +299,10 @@ fn build_chunk(
let should_recurse = !candidate.error
&& recurse.is_some()
&& !should_collapse
&& (candidate.force_recurse || line_count > *LEAF_THRESHOLD || recurse_parse_errors > 0);
&& (candidate.force_recurse
|| recurse_parse_errors > 0
|| (line_count > *LEAF_THRESHOLD
&& recursion_narrows_scope(line_count, &child_candidates)));
let children = if should_recurse {
child_candidates
.into_iter()
@@ -520,6 +523,22 @@ fn assign_unique_names(mut candidates: Vec<RawChunkCandidate<'_>>) -> Vec<RawChu
// ── Collapse heuristics ──────────────────────────────────────────────────
/// Returns `true` when splitting a parent into children actually provides
/// meaningful scope narrowing. Recursion is only worthwhile if addressing
/// the largest child saves at least `PI_CHUNK_MIN_SAVINGS` lines compared
/// to addressing the parent directly.
fn recursion_narrows_scope(parent_lines: usize, children: &[RawChunkCandidate<'_>]) -> bool {
if children.is_empty() {
return false;
}
let max_child_lines = children
.iter()
.map(|c| line_span(c.range_start_line, c.range_end_line))
.max()
.unwrap_or(0);
parent_lines.saturating_sub(max_child_lines) >= *MIN_RECURSE_SAVINGS
}
fn should_collapse_trivial_children(
parent: &RawChunkCandidate<'_>,
children: &[RawChunkCandidate<'_>],
+15 -20
View File
@@ -1,17 +1,20 @@
use std::collections::HashMap;
use crate::chunk::types::{ChunkNode, ChunkTree, RenderChunkTreeParams, VisibleLineRange};
use crate::{
chunk::types::{ChunkNode, ChunkTree, RenderChunkTreeParams, VisibleLineRange},
env_uint,
};
type ChunkLookup<'a> = HashMap<&'a str, &'a ChunkNode>;
use std::sync::LazyLock;
pub static DEFAULT_FULL_DISPLAY_THRESHOLD: LazyLock<usize> =
LazyLock::new(|| read_env_usize("PI_CHUNK_FULL_DISPLAY_THRESHOLD", 80));
pub static DEFAULT_PREVIEW_HEAD_LINES: LazyLock<usize> =
LazyLock::new(|| read_env_usize("PI_CHUNK_PREVIEW_HEAD_LINES", 20));
pub static DEFAULT_PREVIEW_TAIL_LINES: LazyLock<usize> =
LazyLock::new(|| read_env_usize("PI_CHUNK_PREVIEW_TAIL_LINES", 8));
env_uint! {
// Configured full display threshold.
static FULL_DISPLAY_THRESHOLD: usize = "PI_CHUNK_FULL_DISPLAY_THRESHOLD" or 80 => [1, usize::MAX];
// Configured preview head lines.
static PREVIEW_HEAD_LINES: usize = "PI_CHUNK_PREVIEW_HEAD_LINES" or 20 => [1, usize::MAX];
// Configured preview tail lines.
static PREVIEW_TAIL_LINES: usize = "PI_CHUNK_PREVIEW_TAIL_LINES" or 8 => [1, usize::MAX];
}
pub fn line_to_containing_chunk_path(tree: &ChunkTree, line: u32) -> Option<String> {
line_to_containing_chunk(tree, line).map(|chunk| chunk.path.clone())
@@ -23,9 +26,9 @@ pub fn render_chunk_tree(params: &RenderChunkTreeParams) -> String {
return String::new();
};
let source_lines: Vec<&str> = params.source.split('\n').collect();
let full_display_threshold = *DEFAULT_FULL_DISPLAY_THRESHOLD;
let preview_head_lines = *DEFAULT_PREVIEW_HEAD_LINES;
let preview_tail_lines = *DEFAULT_PREVIEW_TAIL_LINES;
let full_display_threshold = *FULL_DISPLAY_THRESHOLD;
let preview_head_lines = *PREVIEW_HEAD_LINES;
let preview_tail_lines = *PREVIEW_TAIL_LINES;
let tab_replacement = params.tab_replacement.as_deref().unwrap_or(" ");
let num_width = compute_num_width(
&params.tree,
@@ -117,14 +120,6 @@ pub fn render_chunk_tree(params: &RenderChunkTreeParams) -> String {
ctx.out
}
fn read_env_usize(name: &str, default_value: usize) -> usize {
std::env::var(name)
.ok()
.and_then(|value| value.parse::<usize>().ok())
.filter(|value| *value > 0)
.unwrap_or(default_value)
}
fn build_lookup(tree: &ChunkTree) -> ChunkLookup<'_> {
tree
.chunks
+14 -26
View File
@@ -22,7 +22,7 @@ use ignore::WalkBuilder;
use napi::bindgen_prelude::*;
use napi_derive::napi;
use crate::task;
use crate::{env_uint, task};
// ═══════════════════════════════════════════════════════════════════════════
// Public types (re-exported by glob for backward compatibility)
@@ -57,36 +57,25 @@ pub struct GlobMatch {
// Cache policy
// ═══════════════════════════════════════════════════════════════════════════
const DEFAULT_CACHE_TTL_MS: u64 = 1_000;
const DEFAULT_EMPTY_RECHECK_MS: u64 = 200;
const DEFAULT_MAX_CACHE_ENTRIES: usize = 16;
fn env_u64(name: &str, default: u64) -> u64 {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
env_uint! {
// Configured cache TTL in milliseconds.
static CACHE_TTL_MS: u64 = "FS_SCAN_CACHE_TTL_MS" or 1_000 => [0, u64::MAX];
// Configured empty-result recheck threshold in milliseconds.
static EMPTY_RECHECK_MS: u64 = "FS_SCAN_EMPTY_RECHECK_MS" or 200 => [0, u64::MAX];
// Configured maximum number of cache entries.
static MAX_CACHE_ENTRIES: usize = "FS_SCAN_CACHE_MAX_ENTRIES" or 16 => [0, usize::MAX];
}
fn env_usize(name: &str, default: usize) -> usize {
std::env::var(name)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(default)
}
/// Configured cache TTL in milliseconds.
pub fn cache_ttl_ms() -> u64 {
env_u64("FS_SCAN_CACHE_TTL_MS", DEFAULT_CACHE_TTL_MS)
*CACHE_TTL_MS
}
/// Configured empty-result recheck threshold in milliseconds.
pub fn empty_recheck_ms() -> u64 {
env_u64("FS_SCAN_EMPTY_RECHECK_MS", DEFAULT_EMPTY_RECHECK_MS)
*EMPTY_RECHECK_MS
}
fn max_cache_entries() -> usize {
env_usize("FS_SCAN_CACHE_MAX_ENTRIES", DEFAULT_MAX_CACHE_ENTRIES)
pub fn max_cache_entries() -> usize {
*MAX_CACHE_ENTRIES
}
// ═══════════════════════════════════════════════════════════════════════════
@@ -117,8 +106,7 @@ pub struct ScanResult {
}
fn evict_oldest() {
let max = max_cache_entries();
if FS_CACHE.len() > max
if FS_CACHE.len() > *MAX_CACHE_ENTRIES
&& let Some(oldest_key) = FS_CACHE
.iter()
.min_by_key(|entry| entry.value().created_at)
@@ -295,7 +283,7 @@ pub fn get_or_scan(
use_gitignore: bool,
ct: &task::CancelToken,
) -> Result<ScanResult> {
let ttl = cache_ttl_ms();
let ttl = *CACHE_TTL_MS;
if ttl == 0 {
// Caching disabled – always scan fresh.
let entries = collect_entries(root, include_hidden, use_gitignore, ct)?;
+34 -3
View File
@@ -1,4 +1,35 @@
/// Saturating cast from `u64` to `u32`, clamping at [`u32::MAX`].
pub fn clamp_u32(value: u64) -> u32 {
value.min(u32::MAX as u64) as u32
#[macro_export]
macro_rules! env_uint {
// With clamp range: ... => [$min, $max];
($( $vis:vis static $name:ident : $type:ty = $env:literal or $default:expr => [$min:expr, $max:expr];)*) => {
$(
$vis static $name: std::sync::LazyLock<$type> = std::sync::LazyLock::new(|| {
std::env::var($env)
.ok()
.and_then(|v| std::str::FromStr::from_str(&v).ok())
.unwrap_or($default)
.clamp($min, $max)
});
)*
};
// Without clamp range: ...; (no => [])
($( $vis:vis static $name:ident : $type:ty = $env:literal or $default:expr;)*) => {
$(
$vis static $name: std::sync::LazyLock<$type> = std::sync::LazyLock::new(|| {
std::env::var($env)
.ok()
.and_then(|v| std::str::FromStr::from_str(&v).ok())
.unwrap_or($default)
});
)*
};
}
/// Saturating cast from `u64` to `u32`, clamping at [`u32::MAX`].
pub const fn clamp_u32(value: u64) -> u32 {
if value > u32::MAX as u64 {
u32::MAX
} else {
value as u32
}
}