nu_lint/
span.rs

1use std::ops::Range;
2
3/// A span in global coordinate space (from AST, includes stdlib offset)
4///
5/// All spans obtained from AST nodes (expressions, calls, blocks) are global.
6/// These cannot be used directly for slicing the source file.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct GlobalSpan {
9    pub start: usize,
10    pub end: usize,
11}
12
13/// A span relative to the current file being linted (starts at 0)
14///
15/// Use for:
16/// - Creating `Replacement` spans
17/// - Regex match positions on `whole_source()`
18/// - Manual line/column calculations
19/// - Slicing source strings
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct FileSpan {
22    pub start: usize,
23    pub end: usize,
24}
25
26/// A span that can be either global (AST) or file-relative
27///
28/// Rules return this type, and the engine normalizes all to `FileSpan` before
29/// output.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum LintSpan {
32    Global(GlobalSpan),
33    File(FileSpan),
34}
35
36impl GlobalSpan {
37    #[must_use]
38    pub const fn new(start: usize, end: usize) -> Self {
39        Self { start, end }
40    }
41
42    /// Convert to file-relative span by subtracting the file offset
43    #[must_use]
44    pub const fn to_file_span(self, file_offset: usize) -> FileSpan {
45        FileSpan {
46            start: self.start.saturating_sub(file_offset),
47            end: self.end.saturating_sub(file_offset),
48        }
49    }
50
51    /// Create a span that encompasses both self and other
52    #[must_use]
53    pub fn merge(self, other: Self) -> Self {
54        Self {
55            start: self.start.min(other.start),
56            end: self.end.max(other.end),
57        }
58    }
59
60    #[must_use]
61    pub const fn len(&self) -> usize {
62        self.end.saturating_sub(self.start)
63    }
64
65    #[must_use]
66    pub const fn is_empty(&self) -> bool {
67        self.start >= self.end
68    }
69}
70
71impl FileSpan {
72    #[must_use]
73    pub const fn new(start: usize, end: usize) -> Self {
74        Self { start, end }
75    }
76
77    /// Convert to global span by adding the file offset
78    #[must_use]
79    pub const fn to_global_span(self, file_offset: usize) -> GlobalSpan {
80        GlobalSpan {
81            start: self.start + file_offset,
82            end: self.end + file_offset,
83        }
84    }
85
86    /// Create a span that encompasses both self and other
87    #[must_use]
88    pub fn merge(self, other: Self) -> Self {
89        Self {
90            start: self.start.min(other.start),
91            end: self.end.max(other.end),
92        }
93    }
94
95    #[must_use]
96    pub const fn len(&self) -> usize {
97        self.end.saturating_sub(self.start)
98    }
99
100    #[must_use]
101    pub const fn is_empty(&self) -> bool {
102        self.start >= self.end
103    }
104
105    #[must_use]
106    pub const fn as_range(&self) -> Range<usize> {
107        self.start..self.end
108    }
109}
110
111impl LintSpan {
112    /// Convert to file-relative span, normalizing if needed
113    #[must_use]
114    pub const fn to_file_span(self, file_offset: usize) -> FileSpan {
115        match self {
116            Self::Global(g) => g.to_file_span(file_offset),
117            Self::File(f) => f,
118        }
119    }
120
121    /// Get the file-relative span, panicking if not normalized.
122    ///
123    /// This should only be called after `normalize_spans()` has been invoked.
124    #[must_use]
125    pub fn file_span(&self) -> FileSpan {
126        match self {
127            Self::File(f) => *f,
128            Self::Global(_) => panic!("Span not normalized - call normalize_spans first"),
129        }
130    }
131}
132
133impl From<nu_protocol::Span> for GlobalSpan {
134    fn from(span: nu_protocol::Span) -> Self {
135        Self {
136            start: span.start,
137            end: span.end,
138        }
139    }
140}
141
142impl From<GlobalSpan> for nu_protocol::Span {
143    fn from(span: GlobalSpan) -> Self {
144        Self::new(span.start, span.end)
145    }
146}
147
148impl From<FileSpan> for nu_protocol::Span {
149    fn from(span: FileSpan) -> Self {
150        Self::new(span.start, span.end)
151    }
152}
153
154impl From<LintSpan> for nu_protocol::Span {
155    fn from(span: LintSpan) -> Self {
156        match span {
157            LintSpan::Global(g) => Self::new(g.start, g.end),
158            LintSpan::File(f) => Self::new(f.start, f.end),
159        }
160    }
161}
162
163impl From<nu_protocol::Span> for LintSpan {
164    fn from(span: nu_protocol::Span) -> Self {
165        Self::Global(GlobalSpan::from(span))
166    }
167}
168
169impl From<GlobalSpan> for LintSpan {
170    fn from(span: GlobalSpan) -> Self {
171        Self::Global(span)
172    }
173}
174
175impl From<FileSpan> for LintSpan {
176    fn from(span: FileSpan) -> Self {
177        Self::File(span)
178    }
179}