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