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::operators::OperatorGrammar;
12use crate::{Parser, SNode};
13
14/// Parse one `${...}` hole into an expression whose spans address `source`.
15///
16/// `segment`, `line`, and `column` come from a
17/// [`harn_lexer::StringSegment::Expression`]; `source` is the whole file that
18/// segment was lexed from. Pass `None` only when the containing file is not
19/// available — the expression still parses, but its spans stay relative to the
20/// hole and must not be used to edit the file.
21pub fn parse_expression(
22    source: Option<&str>,
23    segment: &str,
24    line: usize,
25    column: usize,
26) -> Option<SNode> {
27    parse_expression_with_grammar(OperatorGrammar::CURRENT, source, segment, line, column)
28}
29
30/// [`parse_expression`] under an explicit operator grammar. Only the syntax
31/// migration reads holes with [`OperatorGrammar::LEGACY`].
32pub fn parse_expression_with_grammar(
33    grammar: OperatorGrammar,
34    source: Option<&str>,
35    segment: &str,
36    line: usize,
37    column: usize,
38) -> Option<SNode> {
39    Parser::new(lexer(source, segment, line, column).tokenize().ok()?)
40        .with_operator_grammar(grammar)
41        .parse_single_expression()
42        .ok()
43}
44
45/// Build the lexer `parse_expression` uses, for callers that need the tokens.
46pub fn lexer<'seg>(
47    source: Option<&str>,
48    segment: &'seg str,
49    line: usize,
50    column: usize,
51) -> Lexer<'seg> {
52    let offset = source
53        .and_then(|source| harn_lexer::byte_offset_for_position(source, line, column))
54        .unwrap_or_default();
55    Lexer::with_position_and_offset(segment, line, column, offset)
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::Node;
62
63    #[test]
64    #[expect(
65        clippy::string_slice,
66        reason = "expression spans come from the lexer and lie on char boundaries"
67    )]
68    fn parses_a_hole_into_spans_that_address_the_containing_source() {
69        let source = "const label = \"é ${platform()}\"\n";
70
71        let expression = parse_expression(Some(source), "platform()", 1, 20).expect("expression");
72
73        assert!(matches!(expression.node, Node::FunctionCall { .. }));
74        assert_eq!(
75            &source[expression.span.start..expression.span.end],
76            "platform()"
77        );
78    }
79}