Skip to main content

harn_parser/
interpolation.rs

1//! Shared parsing for `${...}` expression holes.
2//!
3//! The lexer stores an interpolation hole as source text plus the line and
4//! column where it starts. Everything that needs the expression back — the
5//! typechecker, the linter, and `harn fix` — parses it through here, so every
6//! consumer sees spans in the containing file's coordinates rather than
7//! offsets relative to the hole (harn#5850).
8
9use harn_lexer::Lexer;
10
11use crate::{Parser, SNode};
12
13/// Parse one `${...}` hole into an expression whose spans address `source`.
14///
15/// `segment`, `line`, and `column` come from a
16/// [`harn_lexer::StringSegment::Expression`]; `source` is the whole file that
17/// segment was lexed from. Pass `None` only when the containing file is not
18/// available — the expression still parses, but its spans stay relative to the
19/// hole and must not be used to edit the file.
20pub fn parse_expression(
21    source: Option<&str>,
22    segment: &str,
23    line: usize,
24    column: usize,
25) -> Option<SNode> {
26    Parser::new(lexer(source, segment, line, column).tokenize().ok()?)
27        .parse_single_expression()
28        .ok()
29}
30
31/// Build the lexer `parse_expression` uses, for callers that need the tokens.
32pub fn lexer<'seg>(
33    source: Option<&str>,
34    segment: &'seg str,
35    line: usize,
36    column: usize,
37) -> Lexer<'seg> {
38    let offset = source
39        .and_then(|source| harn_lexer::byte_offset_for_position(source, line, column))
40        .unwrap_or_default();
41    Lexer::with_position_and_offset(segment, line, column, offset)
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use crate::Node;
48
49    #[test]
50    #[expect(
51        clippy::string_slice,
52        reason = "expression spans come from the lexer and lie on char boundaries"
53    )]
54    fn parses_a_hole_into_spans_that_address_the_containing_source() {
55        let source = "const label = \"é ${platform()}\"\n";
56
57        let expression = parse_expression(Some(source), "platform()", 1, 20).expect("expression");
58
59        assert!(matches!(expression.node, Node::FunctionCall { .. }));
60        assert_eq!(
61            &source[expression.span.start..expression.span.end],
62            "platform()"
63        );
64    }
65}