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