boxferry_compose/
source.rs1use std::collections::BTreeMap;
4
5use boxferry_engine::{
6 DiagnosticField, DiagnosticValue, NativeFinding, NativeFindingLabel, NativeFindingLabelKind, Severity,
7};
8use boxferry_model::{Identifier, ModelError, SourceId};
9use compose_lens::{
10 diagnostic::{Diagnostic as ComposeDiagnostic, LabelKind, Severity as ComposeSeverity},
11 merge::MergedProject,
12 profiles::ProfileSelection,
13 source::SourceId as ComposeSourceId,
14};
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18#[non_exhaustive]
19pub enum ComposeFindingStage {
20 Load,
22 Interpolation,
24 Merge,
26 ProfileSelection,
28 ProjectModel,
30 Validation,
32}
33
34impl ComposeFindingStage {
35 pub(crate) const fn as_str(self) -> &'static str {
36 match self {
37 Self::Load => "load",
38 Self::Interpolation => "interpolation",
39 Self::Merge => "merge",
40 Self::ProfileSelection => "profile-selection",
41 Self::ProjectModel => "project-model",
42 Self::Validation => "validation",
43 }
44 }
45
46 #[must_use]
48 pub fn native_finding(self, diagnostic: &ComposeDiagnostic) -> NativeFinding {
49 let severity = match diagnostic.severity() {
50 ComposeSeverity::Error => Severity::Error,
51 ComposeSeverity::Warning => Severity::Warning,
52 ComposeSeverity::Note => Severity::Note,
53 };
54 let mut finding = NativeFinding::new(
55 "compose",
56 "compose-lens",
57 diagnostic.code().as_str(),
58 self.as_str(),
59 severity,
60 diagnostic.message(),
61 );
62 if let Some(variable) = interpolation_variable(diagnostic.code().as_str(), diagnostic.message()) {
63 finding = finding.with_field(DiagnosticField::new("variable", DiagnosticValue::plain(variable)));
64 }
65 for label in diagnostic.labels() {
66 finding = finding.with_label(NativeFindingLabel::new(
67 match label.kind() {
68 LabelKind::Primary => NativeFindingLabelKind::Primary,
69 LabelKind::Secondary => NativeFindingLabelKind::Secondary,
70 },
71 label.span().source_id().get(),
72 label.span().start(),
73 label.span().end(),
74 label.message(),
75 ));
76 }
77 for note in diagnostic.notes() {
78 finding = finding.with_note(note);
79 }
80 finding
81 }
82}
83
84#[derive(Clone, Debug, Eq, PartialEq)]
86pub struct ComposeSource {
87 project: MergedProject,
88 fallback_application_name: Identifier,
89 source_ids: BTreeMap<ComposeSourceId, SourceId>,
90 profile_selection: Option<ProfileSelection>,
91 native_findings: Vec<NativeFinding>,
92}
93
94impl ComposeSource {
95 pub fn new(project: MergedProject, fallback_application_name: Identifier) -> Result<Self, ModelError> {
109 let source_ids = project
110 .source_ids()
111 .iter()
112 .copied()
113 .map(|source_id| {
114 SourceId::new(format!("compose-source-{}", source_id.get())).map(|neutral| (source_id, neutral))
115 })
116 .collect::<Result<_, _>>()?;
117 Ok(Self {
118 project,
119 fallback_application_name,
120 source_ids,
121 profile_selection: None,
122 native_findings: Vec::new(),
123 })
124 }
125
126 #[must_use]
131 pub fn with_source_id(mut self, compose: ComposeSourceId, neutral: SourceId) -> Self {
132 self.source_ids.insert(compose, neutral);
133 self
134 }
135
136 #[must_use]
138 pub fn with_profile_selection(mut self, selection: ProfileSelection) -> Self {
139 self.profile_selection = Some(selection);
140 self
141 }
142
143 #[must_use]
148 pub fn with_native_diagnostics<'a>(
149 mut self,
150 stage: ComposeFindingStage,
151 diagnostics: impl IntoIterator<Item = &'a ComposeDiagnostic>,
152 ) -> Self {
153 self.native_findings.extend(
154 diagnostics
155 .into_iter()
156 .map(|diagnostic| stage.native_finding(diagnostic)),
157 );
158 self
159 }
160
161 #[must_use]
163 pub const fn project(&self) -> &MergedProject {
164 &self.project
165 }
166
167 #[must_use]
169 pub const fn fallback_application_name(&self) -> &Identifier {
170 &self.fallback_application_name
171 }
172
173 #[must_use]
175 pub fn source_id(&self, compose: ComposeSourceId) -> Option<&SourceId> {
176 self.source_ids.get(&compose)
177 }
178
179 #[must_use]
181 pub const fn profile_selection(&self) -> Option<&ProfileSelection> {
182 self.profile_selection.as_ref()
183 }
184
185 #[must_use]
187 pub fn native_findings(&self) -> &[NativeFinding] {
188 &self.native_findings
189 }
190}
191
192pub(crate) fn native_project_finding(diagnostic: &ComposeDiagnostic) -> NativeFinding {
193 ComposeFindingStage::ProjectModel.native_finding(diagnostic)
194}
195
196fn interpolation_variable<'a>(code: &str, message: &'a str) -> Option<&'a str> {
197 if !matches!(
198 code,
199 "compose.interpolation.unset-variable" | "compose.interpolation.required-variable"
200 ) {
201 return None;
202 }
203 let (_, remainder) = message.split_once('`')?;
204 let (variable, _) = remainder.split_once('`')?;
205 let mut bytes = variable.bytes();
206 let first = bytes.next()?;
207 ((first == b'_' || first.is_ascii_alphabetic()) && bytes.all(|byte| byte == b'_' || byte.is_ascii_alphanumeric()))
208 .then_some(variable)
209}