Skip to main content

txtx_core/runbook/
location.rs

1//! Unified types for source location tracking and reference collection
2//!
3//! This module provides shared types used across the runbook collector,
4//! validation system, and LSP implementation to track source locations
5//! and references in txtx files.
6//!
7
8use std::ops::Range;
9
10/// Represents a specific location in a source file
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct SourceLocation {
13    /// The file path
14    pub file: String,
15    /// Line number (1-based)
16    pub line: usize,
17    /// Column number (1-based)
18    pub column: usize,
19}
20
21impl SourceLocation {
22    /// Create a new source location
23    pub fn new(file: String, line: usize, column: usize) -> Self {
24        Self { file, line, column }
25    }
26
27    /// Create a location at the start of a file (1, 1)
28    pub fn at_start(file: String) -> Self {
29        Self { file, line: 1, column: 1 }
30    }
31
32    /// Create a location without file context
33    pub fn without_file(line: usize, column: usize) -> Self {
34        Self {
35            file: String::new(),
36            line,
37            column,
38        }
39    }
40}
41
42/// Maps source spans (byte offsets) to line/column positions
43pub struct SourceMapper<'a> {
44    source: &'a str,
45}
46
47impl<'a> SourceMapper<'a> {
48    /// Create a new source mapper for the given source text
49    pub fn new(source: &'a str) -> Self {
50        Self { source }
51    }
52
53    /// Convert a span (byte range) to a source location
54    pub fn span_to_location(&self, span: &Range<usize>, file: String) -> SourceLocation {
55        let (line, column) = self.span_to_position(span);
56        SourceLocation::new(file, line, column)
57    }
58
59    /// Convert a span to line and column (1-based)
60    pub fn span_to_position(&self, span: &Range<usize>) -> (usize, usize) {
61        let start = span.start;
62        let mut line = 1;
63        let mut col = 1;
64
65        for (i, ch) in self.source.char_indices() {
66            if i >= start {
67                break;
68            }
69            if ch == '\n' {
70                line += 1;
71                col = 1;
72            } else {
73                col += 1;
74            }
75        }
76
77        (line, col)
78    }
79
80    /// Convert an optional span to a location, returning a default if None
81    pub fn optional_span_to_location(
82        &self,
83        span: Option<&Range<usize>>,
84        file: String,
85    ) -> SourceLocation {
86        match span {
87            Some(s) => self.span_to_location(s, file),
88            None => SourceLocation::at_start(file),
89        }
90    }
91
92    /// Convert an optional span to position, returning (1, 1) if None
93    pub fn optional_span_to_position(&self, span: Option<&Range<usize>>) -> (usize, usize) {
94        span.map(|s| self.span_to_position(s)).unwrap_or((1, 1))
95    }
96}
97
98/// Context of where a reference or definition appears in the HCL structure
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum BlockContext {
101    /// Inside an action block
102    Action(String),
103    /// Inside a variable block
104    Variable(String),
105    /// Inside a signer block
106    Signer(String),
107    /// Inside an output block
108    Output(String),
109    /// Inside a flow block
110    Flow(String),
111    /// Inside an addon block
112    Addon(String),
113    /// Unknown or top-level context
114    Unknown,
115}
116
117impl BlockContext {
118    /// Extract the name from the context if available
119    pub fn name(&self) -> Option<&str> {
120        match self {
121            BlockContext::Action(name)
122            | BlockContext::Variable(name)
123            | BlockContext::Signer(name)
124            | BlockContext::Output(name)
125            | BlockContext::Flow(name)
126            | BlockContext::Addon(name) => Some(name),
127            BlockContext::Unknown => None,
128        }
129    }
130
131    /// Get the block type as a string
132    pub fn block_type(&self) -> &str {
133        use crate::types::ConstructType;
134
135        match self {
136            BlockContext::Action(_) => ConstructType::Action.into(),
137            BlockContext::Variable(_) => ConstructType::Variable.into(),
138            BlockContext::Signer(_) => ConstructType::Signer.into(),
139            BlockContext::Output(_) => ConstructType::Output.into(),
140            BlockContext::Flow(_) => ConstructType::Flow.into(),
141            BlockContext::Addon(_) => ConstructType::Addon.into(),
142            BlockContext::Unknown => "unknown",
143        }
144    }
145}
146
147/// Type of reference being tracked
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum ReferenceType {
150    /// Reference to an input (input.*)
151    Input,
152    /// Reference to a variable (var.* or variable.*)
153    Variable,
154    /// Reference to an action (action.*)
155    Action,
156    /// Reference to a signer (signer.*)
157    Signer,
158    /// Reference to a flow input (flow.*)
159    FlowInput,
160    /// Reference to an output (output.*)
161    Output,
162}
163
164/// A reference to an input, variable, or other construct in the runbook
165#[derive(Debug, Clone)]
166pub struct InputReference {
167    /// The name being referenced (e.g., "api_key" in input.api_key)
168    pub name: String,
169    /// Full path as it appears (e.g., "input.api_key")
170    pub full_path: String,
171    /// Location where the reference appears
172    pub location: SourceLocation,
173    /// Context where the reference is used
174    pub context: BlockContext,
175    /// Type of reference
176    pub reference_type: ReferenceType,
177}
178
179impl InputReference {
180    /// Create a new input reference
181    pub fn new(
182        name: String,
183        full_path: String,
184        location: SourceLocation,
185        context: BlockContext,
186        reference_type: ReferenceType,
187    ) -> Self {
188        Self {
189            name,
190            full_path,
191            location,
192            context,
193            reference_type,
194        }
195    }
196
197    /// Create an input reference (input.*)
198    pub fn input(name: String, location: SourceLocation, context: BlockContext) -> Self {
199        let full_path = format!("input.{}", name);
200        Self::new(name, full_path, location, context, ReferenceType::Input)
201    }
202
203    /// Create a variable reference (var.* or variable.*)
204    pub fn variable(name: String, location: SourceLocation, context: BlockContext) -> Self {
205        let full_path = format!("var.{}", name);
206        Self::new(name, full_path, location, context, ReferenceType::Variable)
207    }
208
209    /// Create a flow input reference (flow.*)
210    pub fn flow_input(name: String, location: SourceLocation, context: BlockContext) -> Self {
211        let full_path = format!("flow.{}", name);
212        Self::new(name, full_path, location, context, ReferenceType::FlowInput)
213    }
214
215    /// Create an action reference (action.*)
216    pub fn action(name: String, location: SourceLocation, context: BlockContext) -> Self {
217        let full_path = format!("action.{}", name);
218        Self::new(name, full_path, location, context, ReferenceType::Action)
219    }
220
221    /// Create a signer reference (signer.*)
222    pub fn signer(name: String, location: SourceLocation, context: BlockContext) -> Self {
223        let full_path = format!("signer.{}", name);
224        Self::new(name, full_path, location, context, ReferenceType::Signer)
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_source_location_new() {
234        let loc = SourceLocation::new("test.tx".to_string(), 10, 5);
235        assert_eq!(loc.file, "test.tx");
236        assert_eq!(loc.line, 10);
237        assert_eq!(loc.column, 5);
238    }
239
240    #[test]
241    fn test_source_location_at_start() {
242        let loc = SourceLocation::at_start("test.tx".to_string());
243        assert_eq!(loc.line, 1);
244        assert_eq!(loc.column, 1);
245    }
246
247    #[test]
248    fn test_source_mapper_simple() {
249        let source = "hello world";
250        let mapper = SourceMapper::new(source);
251
252        let (line, col) = mapper.span_to_position(&(0..5));
253        assert_eq!(line, 1);
254        assert_eq!(col, 1);
255
256        let (line, col) = mapper.span_to_position(&(6..11));
257        assert_eq!(line, 1);
258        assert_eq!(col, 7);
259    }
260
261    #[test]
262    fn test_source_mapper_multiline() {
263        let source = "line 1\nline 2\nline 3";
264        let mapper = SourceMapper::new(source);
265
266        // Start of line 1
267        let (line, col) = mapper.span_to_position(&(0..1));
268        assert_eq!(line, 1);
269        assert_eq!(col, 1);
270
271        // Start of line 2 (after first \n at position 6)
272        let (line, col) = mapper.span_to_position(&(7..8));
273        assert_eq!(line, 2);
274        assert_eq!(col, 1);
275
276        // Start of line 3 (after second \n at position 13)
277        let (line, col) = mapper.span_to_position(&(14..15));
278        assert_eq!(line, 3);
279        assert_eq!(col, 1);
280    }
281
282    #[test]
283    fn test_source_mapper_newline_boundary() {
284        let source = "abc\ndefg";
285        let mapper = SourceMapper::new(source);
286
287        // Just before newline
288        let (line, col) = mapper.span_to_position(&(3..4));
289        assert_eq!(line, 1);
290        assert_eq!(col, 4);
291
292        // Just after newline
293        let (line, col) = mapper.span_to_position(&(4..5));
294        assert_eq!(line, 2);
295        assert_eq!(col, 1);
296    }
297
298    #[test]
299    fn test_source_mapper_optional_none() {
300        let source = "test";
301        let mapper = SourceMapper::new(source);
302
303        let loc = mapper.optional_span_to_location(None, "test.tx".to_string());
304        assert_eq!(loc.line, 1);
305        assert_eq!(loc.column, 1);
306    }
307
308    #[test]
309    fn test_block_context_name() {
310        use crate::types::ConstructType;
311
312        let ctx = BlockContext::Action("deploy".to_string());
313        assert_eq!(ctx.name(), Some("deploy"));
314        assert_eq!(ctx.block_type(), ConstructType::Action);
315
316        let ctx = BlockContext::Unknown;
317        assert_eq!(ctx.name(), None);
318        assert_eq!(ctx.block_type(), "unknown");
319    }
320
321    #[test]
322    fn test_input_reference_constructors() {
323        let loc = SourceLocation::new("test.tx".to_string(), 5, 10);
324        let ctx = BlockContext::Action("deploy".to_string());
325
326        let input_ref = InputReference::input("api_key".to_string(), loc.clone(), ctx.clone());
327        assert_eq!(input_ref.name, "api_key");
328        assert_eq!(input_ref.full_path, "input.api_key");
329        assert_eq!(input_ref.reference_type, ReferenceType::Input);
330
331        let var_ref = InputReference::variable("my_var".to_string(), loc.clone(), ctx.clone());
332        assert_eq!(var_ref.full_path, "var.my_var");
333        assert_eq!(var_ref.reference_type, ReferenceType::Variable);
334
335        let flow_ref = InputReference::flow_input("chain_id".to_string(), loc.clone(), ctx);
336        assert_eq!(flow_ref.full_path, "flow.chain_id");
337        assert_eq!(flow_ref.reference_type, ReferenceType::FlowInput);
338    }
339
340    #[test]
341    fn test_block_context_equality() {
342        let ctx1 = BlockContext::Action("deploy".to_string());
343        let ctx2 = BlockContext::Action("deploy".to_string());
344        let ctx3 = BlockContext::Action("other".to_string());
345
346        assert_eq!(ctx1, ctx2);
347        assert_ne!(ctx1, ctx3);
348    }
349}