1use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticLabel, Severity};
4use crate::source::{SourceId, SourceSpan};
5use crate::syntax::SyntaxDocument;
6use std::collections::BTreeMap;
7
8pub const UNSET_VARIABLE: DiagnosticCode = DiagnosticCode::new("compose.interpolation.unset-variable");
10
11pub const REQUIRED_VARIABLE: DiagnosticCode = DiagnosticCode::new("compose.interpolation.required-variable");
13
14pub const INVALID_EXPRESSION: DiagnosticCode = DiagnosticCode::new("compose.interpolation.invalid-expression");
16
17pub const NESTING_LIMIT: DiagnosticCode = DiagnosticCode::new("compose.interpolation.nesting-limit");
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct EnvironmentValue {
23 value: String,
24 sensitive: bool,
25}
26
27impl EnvironmentValue {
28 #[must_use]
30 pub fn plain(value: impl Into<String>) -> Self {
31 Self {
32 value: value.into(),
33 sensitive: false,
34 }
35 }
36
37 #[must_use]
39 pub fn sensitive(value: impl Into<String>) -> Self {
40 Self {
41 value: value.into(),
42 sensitive: true,
43 }
44 }
45
46 #[must_use]
48 pub fn value(&self) -> &str {
49 &self.value
50 }
51
52 #[must_use]
54 pub const fn is_sensitive(&self) -> bool {
55 self.sensitive
56 }
57}
58
59pub trait EnvironmentProvider {
61 fn get(&self, name: &str) -> Option<EnvironmentValue>;
63}
64
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
67pub struct EmptyEnvironment;
68
69impl EnvironmentProvider for EmptyEnvironment {
70 fn get(&self, _name: &str) -> Option<EnvironmentValue> {
71 None
72 }
73}
74
75#[derive(Debug, Clone, Default, PartialEq, Eq)]
77pub struct MapEnvironment {
78 values: BTreeMap<String, EnvironmentValue>,
79}
80
81impl MapEnvironment {
82 #[must_use]
84 pub const fn new() -> Self {
85 Self {
86 values: BTreeMap::new(),
87 }
88 }
89
90 pub fn insert(&mut self, name: impl Into<String>, value: impl Into<String>) -> Option<EnvironmentValue> {
92 self.values.insert(name.into(), EnvironmentValue::plain(value))
93 }
94
95 pub fn insert_sensitive(&mut self, name: impl Into<String>, value: impl Into<String>) -> Option<EnvironmentValue> {
97 self.values.insert(name.into(), EnvironmentValue::sensitive(value))
98 }
99
100 pub fn insert_value(&mut self, name: impl Into<String>, value: EnvironmentValue) -> Option<EnvironmentValue> {
102 self.values.insert(name.into(), value)
103 }
104
105 #[must_use]
107 pub fn len(&self) -> usize {
108 self.values.len()
109 }
110
111 #[must_use]
113 pub fn is_empty(&self) -> bool {
114 self.values.is_empty()
115 }
116}
117
118impl EnvironmentProvider for MapEnvironment {
119 fn get(&self, name: &str) -> Option<EnvironmentValue> {
120 self.values.get(name).cloned()
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
126pub enum MissingVariablePolicy {
127 EmptyWithWarning,
129 PreserveWithWarning,
131 Error,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct InterpolationOptions {
138 missing_variable: MissingVariablePolicy,
139 max_nesting: usize,
140}
141
142impl InterpolationOptions {
143 #[must_use]
145 pub const fn new(missing_variable: MissingVariablePolicy) -> Self {
146 Self {
147 missing_variable,
148 max_nesting: 32,
149 }
150 }
151
152 #[must_use]
154 pub fn with_max_nesting(mut self, max_nesting: usize) -> Self {
155 self.max_nesting = max_nesting.max(1);
156 self
157 }
158
159 #[must_use]
161 pub const fn missing_variable(self) -> MissingVariablePolicy {
162 self.missing_variable
163 }
164
165 #[must_use]
167 pub const fn max_nesting(self) -> usize {
168 self.max_nesting
169 }
170}
171
172impl Default for InterpolationOptions {
173 fn default() -> Self {
174 Self::new(MissingVariablePolicy::EmptyWithWarning)
175 }
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub struct InterpolationInput<'a> {
181 value: &'a str,
182 span: SourceSpan,
183 sensitive: bool,
184}
185
186impl<'a> InterpolationInput<'a> {
187 #[must_use]
189 pub const fn new(value: &'a str, span: SourceSpan) -> Self {
190 Self {
191 value,
192 span,
193 sensitive: false,
194 }
195 }
196
197 #[must_use]
199 pub const fn sensitive(mut self) -> Self {
200 self.sensitive = true;
201 self
202 }
203
204 #[must_use]
206 pub const fn value(self) -> &'a str {
207 self.value
208 }
209
210 #[must_use]
212 pub const fn span(self) -> SourceSpan {
213 self.span
214 }
215
216 #[must_use]
218 pub const fn is_sensitive(self) -> bool {
219 self.sensitive
220 }
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
225pub enum InterpolationOperator {
226 Direct,
228 DefaultIfUnsetOrEmpty,
230 DefaultIfUnset,
232 RequiredIfUnsetOrEmpty,
234 RequiredIfUnset,
236 AlternativeIfSetAndNonEmpty,
238 AlternativeIfSet,
240}
241
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
244pub enum SubstitutionOutcome {
245 Environment,
247 Default,
249 Alternative,
251 Empty,
253 Missing,
255 RequiredMissing,
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
261pub struct Substitution {
262 name: String,
263 operator: InterpolationOperator,
264 outcome: SubstitutionOutcome,
265 span: SourceSpan,
266 sensitive: bool,
267}
268
269impl Substitution {
270 #[must_use]
272 pub fn name(&self) -> &str {
273 &self.name
274 }
275
276 #[must_use]
278 pub const fn operator(&self) -> InterpolationOperator {
279 self.operator
280 }
281
282 #[must_use]
284 pub const fn outcome(&self) -> SubstitutionOutcome {
285 self.outcome
286 }
287
288 #[must_use]
290 pub const fn span(&self) -> SourceSpan {
291 self.span
292 }
293
294 #[must_use]
296 pub const fn is_sensitive(&self) -> bool {
297 self.sensitive
298 }
299}
300
301#[derive(Debug, Clone, PartialEq, Eq)]
303pub struct InterpolationResult {
304 original: String,
305 resolved: String,
306 span: SourceSpan,
307 sensitive: bool,
308 substitutions: Vec<Substitution>,
309 diagnostics: Vec<Diagnostic>,
310}
311
312#[derive(Debug, Clone, PartialEq, Eq)]
314pub struct DocumentInterpolation {
315 source_id: SourceId,
316 values: Vec<InterpolationResult>,
317 diagnostics: Vec<Diagnostic>,
318}
319
320impl DocumentInterpolation {
321 #[must_use]
323 pub const fn source_id(&self) -> SourceId {
324 self.source_id
325 }
326
327 #[must_use]
329 pub fn values(&self) -> &[InterpolationResult] {
330 &self.values
331 }
332
333 #[must_use]
335 pub fn value(&self, span: SourceSpan) -> Option<&InterpolationResult> {
336 self.values.iter().find(|value| value.span == span)
337 }
338
339 #[must_use]
341 pub fn diagnostics(&self) -> &[Diagnostic] {
342 &self.diagnostics
343 }
344
345 #[must_use]
347 pub fn is_valid(&self) -> bool {
348 !self
349 .diagnostics
350 .iter()
351 .any(|diagnostic| diagnostic.severity() == Severity::Error)
352 }
353}
354
355impl InterpolationResult {
356 #[must_use]
358 pub fn original(&self) -> &str {
359 &self.original
360 }
361
362 #[must_use]
367 pub fn resolved(&self) -> &str {
368 &self.resolved
369 }
370
371 #[must_use]
373 pub const fn span(&self) -> SourceSpan {
374 self.span
375 }
376
377 #[must_use]
379 pub const fn is_sensitive(&self) -> bool {
380 self.sensitive
381 }
382
383 #[must_use]
385 pub fn substitutions(&self) -> &[Substitution] {
386 &self.substitutions
387 }
388
389 #[must_use]
391 pub fn diagnostics(&self) -> &[Diagnostic] {
392 &self.diagnostics
393 }
394
395 #[must_use]
397 pub fn is_valid(&self) -> bool {
398 !self
399 .diagnostics
400 .iter()
401 .any(|diagnostic| diagnostic.severity() == Severity::Error)
402 }
403}
404
405#[must_use]
408pub fn interpolate(input: InterpolationInput<'_>, environment: &dyn EnvironmentProvider) -> InterpolationResult {
409 interpolate_with_options(input, environment, InterpolationOptions::default())
410}
411
412#[must_use]
414pub fn interpolate_with_options(
415 input: InterpolationInput<'_>,
416 environment: &dyn EnvironmentProvider,
417 options: InterpolationOptions,
418) -> InterpolationResult {
419 let mut context = Context {
420 environment,
421 options,
422 span: input.span,
423 diagnostics: Vec::new(),
424 substitutions: Vec::new(),
425 };
426 let fragment = context.interpolate_text(input.value, 0);
427 InterpolationResult {
428 original: input.value.to_owned(),
429 resolved: fragment.value,
430 span: input.span,
431 sensitive: input.sensitive || fragment.sensitive,
432 substitutions: context.substitutions,
433 diagnostics: context.diagnostics,
434 }
435}
436
437#[must_use]
442pub fn interpolate_document(document: &SyntaxDocument, environment: &dyn EnvironmentProvider) -> DocumentInterpolation {
443 interpolate_document_with_options(document, environment, InterpolationOptions::default())
444}
445
446#[must_use]
448pub fn interpolate_document_with_options(
449 document: &SyntaxDocument,
450 environment: &dyn EnvironmentProvider,
451 options: InterpolationOptions,
452) -> DocumentInterpolation {
453 let values: Vec<_> = document
454 .interpolatable_value_scalars()
455 .into_iter()
456 .map(|value| interpolate_with_options(InterpolationInput::new(&value.value, value.span), environment, options))
457 .collect();
458 let diagnostics = values
459 .iter()
460 .flat_map(|value| value.diagnostics.iter().cloned())
461 .collect();
462 DocumentInterpolation {
463 source_id: document.source_id(),
464 values,
465 diagnostics,
466 }
467}
468
469#[derive(Debug, Clone, PartialEq, Eq)]
470struct Fragment {
471 value: String,
472 sensitive: bool,
473}
474
475impl Fragment {
476 fn plain(value: impl Into<String>) -> Self {
477 Self {
478 value: value.into(),
479 sensitive: false,
480 }
481 }
482}
483
484struct Context<'a> {
485 environment: &'a dyn EnvironmentProvider,
486 options: InterpolationOptions,
487 span: SourceSpan,
488 diagnostics: Vec<Diagnostic>,
489 substitutions: Vec<Substitution>,
490}
491
492impl Context<'_> {
493 fn interpolate_text(&mut self, text: &str, depth: usize) -> Fragment {
494 if depth > self.options.max_nesting {
495 self.diagnostics.push(
496 Diagnostic::new(
497 NESTING_LIMIT,
498 Severity::Error,
499 "interpolation nesting exceeds the configured safety limit",
500 )
501 .with_label(DiagnosticLabel::primary(self.span, "nested expression limit reached")),
502 );
503 return Fragment::plain(text);
504 }
505
506 let mut resolved = String::with_capacity(text.len());
507 let mut sensitive = false;
508 let mut cursor = 0;
509 while let Some(relative) = text[cursor..].find('$') {
510 let dollar = cursor + relative;
511 resolved.push_str(&text[cursor..dollar]);
512 let after_dollar = dollar + 1;
513 if after_dollar == text.len() {
514 resolved.push('$');
515 cursor = after_dollar;
516 break;
517 }
518
519 let next = text.as_bytes()[after_dollar];
520 if next == b'$' {
521 resolved.push('$');
522 cursor = after_dollar + 1;
523 continue;
524 }
525 if next == b'{' {
526 let expression_start = after_dollar + 1;
527 let Some(close) = find_closing_brace(text, expression_start) else {
528 self.invalid_expression();
529 resolved.push_str(&text[dollar..]);
530 cursor = text.len();
531 break;
532 };
533 let expression = &text[expression_start..close];
534 let original = &text[dollar..=close];
535 let fragment = self.evaluate_braced(expression, original, depth);
536 resolved.push_str(&fragment.value);
537 sensitive |= fragment.sensitive;
538 cursor = close + 1;
539 continue;
540 }
541 if is_name_start(next) {
542 let mut end = after_dollar + 1;
543 while end < text.len() && is_name_continue(text.as_bytes()[end]) {
544 end += 1;
545 }
546 let name = &text[after_dollar..end];
547 let original = &text[dollar..end];
548 let fragment = self.evaluate(name, InterpolationOperator::Direct, "", original, depth);
549 resolved.push_str(&fragment.value);
550 sensitive |= fragment.sensitive;
551 cursor = end;
552 continue;
553 }
554
555 resolved.push('$');
556 cursor = after_dollar;
557 }
558 resolved.push_str(&text[cursor..]);
559 Fragment {
560 value: resolved,
561 sensitive,
562 }
563 }
564
565 fn evaluate_braced(&mut self, expression: &str, original: &str, depth: usize) -> Fragment {
566 let Some((name, operator, operand)) = parse_braced_expression(expression) else {
567 self.invalid_expression();
568 return Fragment::plain(original);
569 };
570 self.evaluate(name, operator, operand, original, depth)
571 }
572
573 fn evaluate(
574 &mut self,
575 name: &str,
576 operator: InterpolationOperator,
577 operand: &str,
578 original: &str,
579 depth: usize,
580 ) -> Fragment {
581 let environment = self.environment.get(name);
582 let is_set = environment.is_some();
583 let is_non_empty = environment.as_ref().is_some_and(|value| !value.value.is_empty());
584
585 let (fragment, outcome) = match operator {
586 InterpolationOperator::Direct => environment.map_or_else(
587 || self.missing_direct(name, original),
588 |value| {
589 let fragment = Fragment {
590 value: value.value,
591 sensitive: value.sensitive,
592 };
593 (fragment, SubstitutionOutcome::Environment)
594 },
595 ),
596 InterpolationOperator::DefaultIfUnsetOrEmpty if !is_non_empty => {
597 (self.interpolate_text(operand, depth + 1), SubstitutionOutcome::Default)
598 }
599 InterpolationOperator::DefaultIfUnset if !is_set => {
600 (self.interpolate_text(operand, depth + 1), SubstitutionOutcome::Default)
601 }
602 InterpolationOperator::RequiredIfUnsetOrEmpty if !is_non_empty => {
603 self.required_missing(name);
604 (Fragment::plain(original), SubstitutionOutcome::RequiredMissing)
605 }
606 InterpolationOperator::RequiredIfUnset if !is_set => {
607 self.required_missing(name);
608 (Fragment::plain(original), SubstitutionOutcome::RequiredMissing)
609 }
610 InterpolationOperator::AlternativeIfSetAndNonEmpty if is_non_empty => (
611 self.interpolate_text(operand, depth + 1),
612 SubstitutionOutcome::Alternative,
613 ),
614 InterpolationOperator::AlternativeIfSet if is_set => (
615 self.interpolate_text(operand, depth + 1),
616 SubstitutionOutcome::Alternative,
617 ),
618 InterpolationOperator::AlternativeIfSetAndNonEmpty | InterpolationOperator::AlternativeIfSet => {
619 (Fragment::plain(""), SubstitutionOutcome::Empty)
620 }
621 InterpolationOperator::DefaultIfUnsetOrEmpty
622 | InterpolationOperator::DefaultIfUnset
623 | InterpolationOperator::RequiredIfUnsetOrEmpty
624 | InterpolationOperator::RequiredIfUnset => {
625 let value = environment.unwrap_or_else(|| EnvironmentValue::plain(""));
626 (
627 Fragment {
628 value: value.value,
629 sensitive: value.sensitive,
630 },
631 SubstitutionOutcome::Environment,
632 )
633 }
634 };
635
636 self.substitutions.push(Substitution {
637 name: name.to_owned(),
638 operator,
639 outcome,
640 span: self.span,
641 sensitive: fragment.sensitive,
642 });
643 fragment
644 }
645
646 fn missing_direct(&mut self, name: &str, original: &str) -> (Fragment, SubstitutionOutcome) {
647 let (severity, value) = match self.options.missing_variable {
648 MissingVariablePolicy::EmptyWithWarning => (Severity::Warning, ""),
649 MissingVariablePolicy::PreserveWithWarning => (Severity::Warning, original),
650 MissingVariablePolicy::Error => (Severity::Error, original),
651 };
652 self.diagnostics.push(
653 Diagnostic::new(
654 UNSET_VARIABLE,
655 severity,
656 format!("interpolation variable `{name}` is not set"),
657 )
658 .with_label(DiagnosticLabel::primary(self.span, "unresolved variable expression")),
659 );
660 (Fragment::plain(value), SubstitutionOutcome::Missing)
661 }
662
663 fn required_missing(&mut self, name: &str) {
664 self.diagnostics.push(
665 Diagnostic::new(
666 REQUIRED_VARIABLE,
667 Severity::Error,
668 format!("required interpolation variable `{name}` is unset or empty"),
669 )
670 .with_label(DiagnosticLabel::primary(self.span, "required variable is unavailable")),
671 );
672 }
673
674 fn invalid_expression(&mut self) {
675 self.diagnostics.push(
676 Diagnostic::new(
677 INVALID_EXPRESSION,
678 Severity::Error,
679 "interpolation expression is malformed or unsupported",
680 )
681 .with_label(DiagnosticLabel::primary(self.span, "invalid interpolation expression")),
682 );
683 }
684}
685
686fn parse_braced_expression(expression: &str) -> Option<(&str, InterpolationOperator, &str)> {
687 let bytes = expression.as_bytes();
688 let first = *bytes.first()?;
689 if !is_name_start(first) {
690 return None;
691 }
692 let mut name_end = 1;
693 while name_end < bytes.len() && is_name_continue(bytes[name_end]) {
694 name_end += 1;
695 }
696 let name = &expression[..name_end];
697 let remainder = &expression[name_end..];
698 if remainder.is_empty() {
699 return Some((name, InterpolationOperator::Direct, ""));
700 }
701
702 for (prefix, operator) in [
703 (":-", InterpolationOperator::DefaultIfUnsetOrEmpty),
704 (":?", InterpolationOperator::RequiredIfUnsetOrEmpty),
705 (":+", InterpolationOperator::AlternativeIfSetAndNonEmpty),
706 ("-", InterpolationOperator::DefaultIfUnset),
707 ("?", InterpolationOperator::RequiredIfUnset),
708 ("+", InterpolationOperator::AlternativeIfSet),
709 ] {
710 if let Some(operand) = remainder.strip_prefix(prefix) {
711 return Some((name, operator, operand));
712 }
713 }
714 None
715}
716
717fn find_closing_brace(text: &str, start: usize) -> Option<usize> {
718 let mut depth = 1usize;
719 let mut cursor = start;
720 while cursor < text.len() {
721 if text[cursor..].starts_with("${") {
722 depth += 1;
723 cursor += 2;
724 continue;
725 }
726 let character = text[cursor..].chars().next()?;
727 if character == '}' {
728 depth -= 1;
729 if depth == 0 {
730 return Some(cursor);
731 }
732 }
733 cursor += character.len_utf8();
734 }
735 None
736}
737
738const fn is_name_start(byte: u8) -> bool {
739 byte == b'_' || byte.is_ascii_alphabetic()
740}
741
742const fn is_name_continue(byte: u8) -> bool {
743 is_name_start(byte) || byte.is_ascii_digit()
744}