From c43aedfc9db23e9be76d7a7a835346f0c9754be0 Mon Sep 17 00:00:00 2001 From: Sephyi Date: Sun, 19 Apr 2026 19:10:43 +0200 Subject: [PATCH] refactor(services): document SAFETY invariants for tree-sitter child cast truncation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a one-line `// SAFETY:` comment immediately above each `#[allow(clippy::cast_possible_truncation)]` in analyzer.rs and differ.rs. Every annotated site performs an `i as u32` cast on a `usize` loop index to call `tree_sitter::Node::child(u32)`. The cast is sound because: 1. tree-sitter nodes cannot have more than `u32::MAX` children — the upstream API (`child_count() -> usize`) is bounded by an internally-stored `u32` count. 2. Each cast site's index originates from a loop bounded by `child_count()`, so the value is already `<= u32::MAX` by construction and truncation cannot lose information. All 16 sites use identical wording for easy grepping and future audit. No behavioural change. Closes audit entry F-015 from #3. --- src/services/analyzer.rs | 4 ++++ src/services/differ.rs | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/services/analyzer.rs b/src/services/analyzer.rs index e6695ee..751c4f6 100644 --- a/src/services/analyzer.rs +++ b/src/services/analyzer.rs @@ -345,6 +345,7 @@ impl AnalyzerService { target_row: usize, ) -> Option> { for i in 0..parent.child_count() { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] if let Some(child) = parent.child(i as u32) && child.start_position().row <= target_row @@ -530,6 +531,7 @@ impl AnalyzerService { fn has_java_public_modifier(node: tree_sitter::Node) -> bool { let child_count = node.child_count(); for i in 0..child_count { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] if let Some(child) = node.child(i as u32) && child.kind() == "modifiers" @@ -564,6 +566,7 @@ impl AnalyzerService { .or_else(|| { (0..node.child_count()) .filter_map(|i| { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] node.child(i as u32) }) @@ -633,6 +636,7 @@ impl AnalyzerService { fn has_csharp_public_modifier(node: tree_sitter::Node) -> bool { let child_count = node.child_count(); for i in 0..child_count { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] if let Some(child) = node.child(i as u32) && child.kind() == "modifier" diff --git a/src/services/differ.rs b/src/services/differ.rs index 1d04f34..1627014 100644 --- a/src/services/differ.rs +++ b/src/services/differ.rs @@ -151,6 +151,7 @@ impl AstDiffer { // Find body-like node that contains field or variant definitions let body = (0..node.child_count()) .filter_map(|i| { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] node.child(i as u32) }) @@ -169,6 +170,7 @@ impl AstDiffer { if let Some(b) = body { for i in 0..b.child_count() { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] if let Some(child) = b.child(i as u32) { // Skip symbols/punc @@ -273,6 +275,7 @@ impl AstDiffer { // Look for type_parameters child (Rust, TS, Java) (0..node.child_count()) .filter_map(|i| { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] node.child(i as u32) }) @@ -298,6 +301,7 @@ impl AstDiffer { let param_node = node.child_by_field_name("parameters").or_else(|| { (0..node.child_count()) .filter_map(|i| { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] node.child(i as u32) }) @@ -311,6 +315,7 @@ impl AstDiffer { if let Some(pnode) = param_node { for i in 0..pnode.child_count() { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] if let Some(child) = pnode.child(i as u32) { // Skip delimiters like ( ) , @@ -398,6 +403,7 @@ impl AstDiffer { // Check for visibility_modifier child (Rust) (0..node.child_count()) .filter_map(|i| { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] node.child(i as u32) }) @@ -414,6 +420,7 @@ impl AstDiffer { fn is_async(node: tree_sitter::Node) -> bool { (0..node.child_count()) .filter_map(|i| { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] node.child(i as u32) }) @@ -427,6 +434,7 @@ impl AstDiffer { fn has_keyword(node: tree_sitter::Node, keyword: &str) -> bool { (0..node.child_count()) .filter_map(|i| { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] node.child(i as u32) }) @@ -438,6 +446,7 @@ impl AstDiffer { if c.kind().ends_with("_modifiers") || c.kind() == "modifiers" { return (0..c.child_count()) .filter_map(|j| { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] c.child(j as u32) }) @@ -451,6 +460,7 @@ impl AstDiffer { fn extract_derives(node: tree_sitter::Node, source: &str) -> Vec { let mut derives = Vec::new(); for i in 0..node.child_count() { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] let Some(child) = node.child(i as u32) else { continue; @@ -493,6 +503,7 @@ impl AstDiffer { let body = node.child_by_field_name("body").or_else(|| { (0..node.child_count()) .filter_map(|i| { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] node.child(i as u32) }) @@ -527,6 +538,7 @@ mod tests { let root = tree.root_node(); (0..root.child_count()) .filter_map(|i| { + // SAFETY: tree-sitter nodes cannot have > u32::MAX children; loop index is bounded by child_count(). #[allow(clippy::cast_possible_truncation)] root.child(i as u32) })