Visit a binding node. Classify as Function, Module, or Const based on the value.
(state: &mut ExtractionState, node: TsNode<'_>)
| 196 | |
| 197 | /// Visit a binding node. Classify as Function, Module, or Const based on the value. |
| 198 | fn visit_binding(state: &mut ExtractionState, node: TsNode<'_>) { |
| 199 | // Extract name from attrpath child |
| 200 | let name = Self::extract_binding_name(state, node); |
| 201 | let Some(name) = name else { |
| 202 | return; |
| 203 | }; |
| 204 | |
| 205 | // Get the expression (value) child |
| 206 | let expr = node.child_by_field_name("expression"); |
| 207 | |
| 208 | let docstring = Self::extract_docstring(state, node); |
| 209 | let start_line = node.start_position().row as u32; |
| 210 | let end_line = node.end_position().row as u32; |
| 211 | let start_column = node.start_position().column as u32; |
| 212 | let end_column = node.end_position().column as u32; |
| 213 | |
| 214 | // Enhancement 3: Flake output schema awareness. |
| 215 | // In flake.nix files, known output attrs are forced to Module classification. |
| 216 | let force_module = Self::is_flake_file(state) && Self::is_flake_output_attr(&name); |
| 217 | |
| 218 | // Enhancement 1: Check if the expression is a builder call (derivation). |
| 219 | // We store the builder info before classifying to avoid borrow issues. |
| 220 | let builder_info: Option<(String, usize)> = expr.and_then(|e| { |
| 221 | Self::is_builder_call(state, e).map(|(_callee, attrset)| { |
| 222 | // Store the attrset node id so we can find it again |
| 223 | (String::new(), attrset.id()) |
| 224 | }) |
| 225 | }); |
| 226 | let is_builder = builder_info.is_some(); |
| 227 | |
| 228 | // Classify based on value expression type |
| 229 | let classification = if force_module { |
| 230 | Some(BindingKind::Module) |
| 231 | } else { |
| 232 | expr.map(|e| Self::classify_expression(e)) |
| 233 | }; |
| 234 | |
| 235 | match classification { |
| 236 | Some(BindingKind::Function) => { |
| 237 | let kind = NodeKind::Function; |
| 238 | let qualified_name = format!("{}::{}", state.qualified_prefix(), name); |
| 239 | let id = generate_node_id(&state.file_path, &kind, &name, start_line); |
| 240 | let signature = Self::extract_function_signature(state, node); |
| 241 | let metrics = if let Some(expr_node) = expr { |
| 242 | if expr_node.child_count() > 0 { |
| 243 | count_complexity(expr_node, &NIX_COMPLEXITY, &state.source) |
| 244 | } else { |
| 245 | ComplexityMetrics::default() |
| 246 | } |
| 247 | } else { |
| 248 | ComplexityMetrics::default() |
| 249 | }; |
| 250 | |
| 251 | let graph_node = Node { |
| 252 | id: id.clone(), |
| 253 | kind, |
| 254 | name: name.clone(), |
| 255 | qualified_name, |
nothing calls this directly
no test coverage detected