1use crate::diagnostic_projection::{
2 core_error_diagnostic, flowchart_facts_projection_diagnostic, no_diagram_diagnostic,
3 panic_diagnostic,
4};
5use crate::recovery::{
6 AnalysisRecoveryDiagnostic, core_error_recovery_diagnostics, editor_recovery_diagnostics,
7 merge_recovery_diagnostics,
8};
9use crate::rules::AnalysisRuleConfig;
10use crate::{
11 AnalysisDiagnostic, AnalysisFlowchartFacts, AnalysisPayload, AnalysisResult,
12 AnalysisSyntaxFacts, AnalyzedDiagram, DocumentDiagram, FenceTextIndex, SourceDescriptor,
13 SourceMap,
14};
15use merman_core::{
16 EditorSemanticFacts, Engine, Error as CoreError, MermaidConfig, ParseOptions, ParsedDiagram,
17 ParsedDiagramWithEditorFacts, ParsedEditorFacts,
18};
19use std::panic::{self, AssertUnwindSafe};
20use std::sync::Arc;
21
22#[derive(Debug, Clone, PartialEq)]
23pub struct AnalysisOptions {
24 pub parse: ParseOptions,
25 pub source: SourceDescriptor,
26 pub site_config: Option<MermaidConfig>,
27 pub fixed_today: Option<chrono::NaiveDate>,
28 pub fixed_local_offset_minutes: Option<i32>,
29 pub max_source_bytes: Option<usize>,
30 pub rule_config: AnalysisRuleConfig,
31}
32
33impl Default for AnalysisOptions {
34 fn default() -> Self {
35 Self {
36 parse: ParseOptions::strict(),
37 source: SourceDescriptor::diagram(),
38 site_config: None,
39 fixed_today: None,
40 fixed_local_offset_minutes: None,
41 max_source_bytes: None,
42 rule_config: AnalysisRuleConfig::default(),
43 }
44 }
45}
46
47impl AnalysisOptions {
48 pub fn with_parse_options(mut self, parse: ParseOptions) -> Self {
49 self.parse = parse;
50 self
51 }
52
53 pub fn with_source(mut self, source: SourceDescriptor) -> Self {
54 self.source = source;
55 self
56 }
57
58 pub fn with_site_config(mut self, site_config: MermaidConfig) -> Self {
59 self.site_config = Some(site_config);
60 self
61 }
62
63 pub fn with_fixed_today(mut self, today: Option<chrono::NaiveDate>) -> Self {
64 self.fixed_today = today;
65 self
66 }
67
68 pub fn with_fixed_local_offset_minutes(mut self, offset_minutes: Option<i32>) -> Self {
69 self.fixed_local_offset_minutes = offset_minutes;
70 self
71 }
72
73 pub fn with_max_source_bytes(mut self, max_source_bytes: Option<usize>) -> Self {
74 self.max_source_bytes = max_source_bytes;
75 self
76 }
77
78 pub fn with_rule_config(mut self, rule_config: AnalysisRuleConfig) -> Self {
79 self.rule_config = rule_config;
80 self
81 }
82
83 pub fn snapshot_affecting_eq(&self, other: &Self) -> bool {
84 self.parse == other.parse
85 && self.site_config == other.site_config
86 && self.fixed_today == other.fixed_today
87 && self.fixed_local_offset_minutes == other.fixed_local_offset_minutes
88 && self.max_source_bytes == other.max_source_bytes
89 && self.source == other.source
90 }
91}
92
93#[derive(Debug, Clone)]
94pub struct Analyzer {
95 engine: Engine,
96 options: AnalysisOptions,
97}
98
99impl Default for Analyzer {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105impl Analyzer {
106 pub fn new() -> Self {
107 Self::with_options(AnalysisOptions::default())
108 }
109
110 pub fn with_options(options: AnalysisOptions) -> Self {
111 let engine = engine_from_options(&options);
112 Self { engine, options }
113 }
114
115 pub fn with_engine_and_options(engine: Engine, options: AnalysisOptions) -> Self {
116 Self { engine, options }
117 }
118
119 pub fn options(&self) -> &AnalysisOptions {
120 &self.options
121 }
122
123 pub fn analyze_result(&self, source: &str) -> AnalysisResult {
124 if let Some(result) = self.source_limit_result(source, self.options.source.clone()) {
125 return result;
126 }
127
128 let source_text = Arc::<str>::from(source);
129 let source_map = SourceMap::new(Arc::clone(&source_text));
130 let diagram = crate::document::whole_document_diagram(source_text, &self.options.source);
131 let analyzed = self.analyze_diagram(&diagram);
132 AnalysisResult::new(
133 self.options.source.clone(),
134 source_map,
135 analyzed.diagnostics.clone(),
136 vec![analyzed],
137 )
138 }
139
140 pub fn analyze(&self, source: &str) -> AnalysisPayload {
141 if let Some(result) = self.source_limit_result(source, self.options.source.clone()) {
142 return result.into_payload();
143 }
144
145 AnalysisPayload::new(
146 self.options.source.clone(),
147 self.analyze_source_diagnostics(source),
148 )
149 }
150
151 pub fn analyze_facts(&self, source: &str) -> crate::AnalysisFactsPayload {
152 self.analyze_result(source).to_facts_payload()
153 }
154
155 pub fn analyze_json(&self, source: &str) -> Result<Vec<u8>, serde_json::Error> {
156 self.analyze(source).to_json_bytes()
157 }
158
159 pub fn analyze_facts_json(&self, source: &str) -> Result<Vec<u8>, serde_json::Error> {
160 self.analyze_facts(source).to_json_bytes()
161 }
162
163 pub(crate) fn analyze_diagram(&self, diagram: &DocumentDiagram) -> AnalyzedDiagram {
164 let local = self.analyze_local(&diagram.text, AnalysisMode::RichFacts);
165 AnalyzedDiagram::from_document_diagram(diagram, local.diagnostics, local.syntax)
166 }
167
168 pub(crate) fn analyze_diagram_diagnostics(
169 &self,
170 diagram: &DocumentDiagram,
171 ) -> Vec<AnalysisDiagnostic> {
172 self.analyze_source_diagnostics(&diagram.text)
173 }
174
175 pub(crate) fn analyze_source_diagnostics(&self, source: &str) -> Vec<AnalysisDiagnostic> {
176 self.analyze_local(source, AnalysisMode::Diagnostics)
177 .diagnostics
178 }
179
180 fn analyze_local(&self, source: &str, mode: AnalysisMode) -> LocalAnalysis {
181 if let Some(diagnostics) = self.source_limit_diagnostics(source) {
182 return LocalAnalysis::empty_syntax(diagnostics);
183 }
184
185 let source_map = SourceMap::new(source);
186
187 if source.trim().is_empty() {
188 let diagnostics = no_diagram_diagnostic(&source_map, &self.options.rule_config)
189 .into_iter()
190 .collect();
191 return mode.text_scan_or_empty(source, None, diagnostics);
192 }
193
194 let source_lints =
195 crate::rules::source_lint_diagnostics(source, &source_map, &self.options.rule_config);
196
197 let parse_result = panic::catch_unwind(AssertUnwindSafe(|| match mode {
198 AnalysisMode::Diagnostics => self
199 .engine
200 .parse_diagram_sync(source, self.options.parse)
201 .map(|parsed| parsed.map(ParsedAnalysisDiagram::Diagnostics)),
202 AnalysisMode::RichFacts => self
203 .engine
204 .parse_diagram_with_editor_facts_sync(source, self.options.parse)
205 .map(|parsed| parsed.map(ParsedAnalysisDiagram::RichFacts)),
206 }));
207
208 match parse_result {
209 Err(panic_payload) => {
210 let mut diagnostics = source_lints;
211 if let Some(diagnostic) =
212 panic_diagnostic(panic_payload, &source_map, &self.options.rule_config)
213 {
214 diagnostics.push(diagnostic);
215 }
216 mode.text_scan_or_empty(source, None, diagnostics)
217 }
218 Ok(parse_result) => match parse_result {
219 Ok(Some(parsed)) => {
220 self.analyze_parsed_diagram(source, &source_map, parsed, source_lints, mode)
221 }
222 Ok(None) => mode.text_scan_or_empty(source, None, source_lints),
223 Err(error) => {
224 self.analyze_parse_error(source, &source_map, source_lints, error, mode)
225 }
226 },
227 }
228 }
229
230 fn analyze_parsed_diagram(
231 &self,
232 source: &str,
233 source_map: &SourceMap,
234 parsed: ParsedAnalysisDiagram,
235 mut diagnostics: Vec<AnalysisDiagnostic>,
236 mode: AnalysisMode,
237 ) -> LocalAnalysis {
238 let (parsed, precomputed_editor_facts) = parsed.into_parts();
239 let diagram_type = parsed.meta.diagram_type;
240 diagnostics.extend(crate::rules::parsed_source_lint_diagnostics(
241 source,
242 source_map,
243 &self.options.rule_config,
244 &diagram_type,
245 ));
246 diagnostics.extend(crate::rules::semantic_warning_diagnostics(
247 &diagram_type,
248 &parsed.model,
249 source_map,
250 &self.options.rule_config,
251 ));
252
253 match mode {
254 AnalysisMode::Diagnostics => {
255 LocalAnalysis::empty_syntax_with_type(Some(diagram_type), diagnostics)
256 }
257 AnalysisMode::RichFacts => {
258 let flowchart_projection =
259 self.flowchart_facts_projection(&parsed.model, &diagram_type, source_map);
260 diagnostics.extend(flowchart_projection.diagnostics);
261 let editor_projection = match precomputed_editor_facts {
262 Some(ParsedEditorFacts::Available(facts)) => self
263 .editor_facts_projection_from_facts(
264 source,
265 &diagram_type,
266 source_map,
267 mode,
268 Some(facts),
269 ),
270 Some(ParsedEditorFacts::Unavailable) => self
271 .editor_facts_projection_from_facts(
272 source,
273 &diagram_type,
274 source_map,
275 mode,
276 None,
277 ),
278 Some(ParsedEditorFacts::Error(error)) => self
279 .editor_facts_projection_from_error(
280 source,
281 &diagram_type,
282 source_map,
283 mode,
284 error,
285 ),
286 None => self.editor_facts_projection(source, &diagram_type, source_map, mode),
287 };
288 diagnostics.extend(
289 editor_projection
290 .diagnostics
291 .into_iter()
292 .map(|recovery| recovery.diagnostic),
293 );
294 LocalAnalysis {
295 diagnostics,
296 syntax: AnalysisSyntaxFacts::new(
297 Some(diagram_type),
298 editor_projection.text_index,
299 )
300 .with_flowchart(flowchart_projection.facts),
301 }
302 }
303 }
304 }
305
306 fn analyze_parse_error(
307 &self,
308 source: &str,
309 source_map: &SourceMap,
310 mut diagnostics: Vec<AnalysisDiagnostic>,
311 error: CoreError,
312 mode: AnalysisMode,
313 ) -> LocalAnalysis {
314 let core_diagnostic = core_error_diagnostic(error, source_map, &self.options.rule_config);
315 if let Some(diagnostic) = core_diagnostic.diagnostic {
316 diagnostics.push(diagnostic);
317 }
318 let syntax = match core_diagnostic.diagram_type {
319 Some(diagram_type) => {
320 let editor_projection =
321 self.editor_facts_projection(source, &diagram_type, source_map, mode);
322 merge_recovery_diagnostics(
323 &mut diagnostics,
324 editor_projection.diagnostics,
325 core_diagnostic.parse_location,
326 );
327 AnalysisSyntaxFacts::new(Some(diagram_type), editor_projection.text_index)
328 }
329 None if mode == AnalysisMode::Diagnostics => {
330 AnalysisSyntaxFacts::new(None, FenceTextIndex::default())
331 }
332 None => AnalysisSyntaxFacts::text_scan(source, None),
333 };
334 LocalAnalysis {
335 diagnostics,
336 syntax,
337 }
338 }
339
340 fn editor_facts_projection(
341 &self,
342 source: &str,
343 diagram_type: &str,
344 source_map: &SourceMap,
345 mode: AnalysisMode,
346 ) -> EditorFactsProjection {
347 match self.parse_editor_semantic_facts(source, diagram_type, source_map) {
348 Err(diagnostics) => {
349 EditorFactsProjection::fallback(source, Some(diagram_type), diagnostics, mode)
350 }
351 Ok(Some(facts)) => self.editor_facts_projection_from_facts(
352 source,
353 diagram_type,
354 source_map,
355 mode,
356 Some(facts),
357 ),
358 Ok(None) => self.editor_facts_projection_from_facts(
359 source,
360 diagram_type,
361 source_map,
362 mode,
363 None,
364 ),
365 }
366 }
367
368 fn editor_facts_projection_from_facts(
369 &self,
370 source: &str,
371 diagram_type: &str,
372 source_map: &SourceMap,
373 mode: AnalysisMode,
374 facts: Option<EditorSemanticFacts>,
375 ) -> EditorFactsProjection {
376 let Some(facts) = facts else {
377 return EditorFactsProjection::fallback(source, Some(diagram_type), Vec::new(), mode);
378 };
379
380 let source_mapped_spans = facts.span_coordinate_space.is_original_source();
381 let diagnostics = editor_recovery_diagnostics(
382 facts.diagnostics.iter().cloned(),
383 diagram_type,
384 source_map,
385 &self.options.rule_config,
386 source_mapped_spans,
387 );
388 EditorFactsProjection {
389 text_index: match mode {
390 AnalysisMode::Diagnostics => FenceTextIndex::default(),
391 AnalysisMode::RichFacts => FenceTextIndex::from_core_facts(facts),
392 },
393 diagnostics,
394 }
395 }
396
397 fn editor_facts_projection_from_error(
398 &self,
399 source: &str,
400 diagram_type: &str,
401 source_map: &SourceMap,
402 mode: AnalysisMode,
403 error: CoreError,
404 ) -> EditorFactsProjection {
405 if matches!(error, CoreError::UnsupportedDiagram { .. }) {
406 return EditorFactsProjection::fallback(source, Some(diagram_type), Vec::new(), mode);
407 }
408
409 let diagnostics =
410 core_error_recovery_diagnostics(error, source_map, &self.options.rule_config);
411 EditorFactsProjection::fallback(source, Some(diagram_type), diagnostics, mode)
412 }
413
414 fn flowchart_facts_projection(
415 &self,
416 model: &serde_json::Value,
417 diagram_type: &str,
418 source_map: &SourceMap,
419 ) -> FlowchartFactsProjection {
420 match AnalysisFlowchartFacts::try_from_model(model) {
421 Ok(facts) => FlowchartFactsProjection {
422 facts,
423 diagnostics: Vec::new(),
424 },
425 Err(error) => FlowchartFactsProjection {
426 facts: None,
427 diagnostics: flowchart_facts_projection_diagnostic(
428 error,
429 diagram_type,
430 source_map,
431 &self.options.rule_config,
432 )
433 .into_iter()
434 .collect(),
435 },
436 }
437 }
438
439 fn parse_editor_semantic_facts(
440 &self,
441 source: &str,
442 diagram_type: &str,
443 source_map: &SourceMap,
444 ) -> Result<Option<EditorSemanticFacts>, Vec<AnalysisRecoveryDiagnostic>> {
445 let facts_result = panic::catch_unwind(AssertUnwindSafe(|| {
446 self.engine.parse_editor_semantic_facts_with_type_sync(
447 diagram_type,
448 source,
449 self.options.parse,
450 )
451 }));
452
453 match facts_result {
454 Err(panic_payload) => {
455 Err(
456 panic_diagnostic(panic_payload, source_map, &self.options.rule_config)
457 .map(AnalysisRecoveryDiagnostic::plain)
458 .into_iter()
459 .collect(),
460 )
461 }
462 Ok(Ok(facts)) => Ok(facts),
463 Ok(Err(CoreError::UnsupportedDiagram { .. })) => Ok(None),
464 Ok(Err(error)) => Err(core_error_recovery_diagnostics(
465 error,
466 source_map,
467 &self.options.rule_config,
468 )),
469 }
470 }
471
472 pub(crate) fn source_limit_result(
473 &self,
474 source: &str,
475 descriptor: SourceDescriptor,
476 ) -> Option<AnalysisResult> {
477 crate::source_limits::source_limit_result(source, descriptor, self.options.max_source_bytes)
478 }
479
480 fn source_limit_diagnostics(&self, source: &str) -> Option<Vec<AnalysisDiagnostic>> {
481 crate::source_limits::source_limit_diagnostics(source, self.options.max_source_bytes)
482 }
483}
484
485#[derive(Debug, Clone, Copy, PartialEq, Eq)]
486enum AnalysisMode {
487 Diagnostics,
488 RichFacts,
489}
490
491enum ParsedAnalysisDiagram {
492 Diagnostics(ParsedDiagram),
493 RichFacts(ParsedDiagramWithEditorFacts),
494}
495
496impl ParsedAnalysisDiagram {
497 fn into_parts(self) -> (ParsedDiagram, Option<ParsedEditorFacts>) {
498 match self {
499 Self::Diagnostics(parsed) => (parsed, None),
500 Self::RichFacts(parsed) => (parsed.diagram, Some(parsed.editor_facts)),
501 }
502 }
503}
504
505impl AnalysisMode {
506 fn text_scan_or_empty(
507 self,
508 source: &str,
509 diagram_type: Option<String>,
510 diagnostics: Vec<AnalysisDiagnostic>,
511 ) -> LocalAnalysis {
512 match self {
513 Self::Diagnostics => LocalAnalysis::empty_syntax_with_type(diagram_type, diagnostics),
514 Self::RichFacts => LocalAnalysis::text_scan(source, diagram_type, diagnostics),
515 }
516 }
517}
518
519#[derive(Debug, Clone)]
520struct LocalAnalysis {
521 diagnostics: Vec<AnalysisDiagnostic>,
522 syntax: AnalysisSyntaxFacts,
523}
524
525impl LocalAnalysis {
526 fn empty_syntax(diagnostics: Vec<AnalysisDiagnostic>) -> Self {
527 Self::empty_syntax_with_type(None, diagnostics)
528 }
529
530 fn empty_syntax_with_type(
531 diagram_type: Option<String>,
532 diagnostics: Vec<AnalysisDiagnostic>,
533 ) -> Self {
534 Self {
535 diagnostics,
536 syntax: AnalysisSyntaxFacts::new(diagram_type, FenceTextIndex::default()),
537 }
538 }
539
540 fn text_scan(
541 source: &str,
542 diagram_type: Option<String>,
543 diagnostics: Vec<AnalysisDiagnostic>,
544 ) -> Self {
545 Self {
546 diagnostics,
547 syntax: AnalysisSyntaxFacts::text_scan(source, diagram_type),
548 }
549 }
550}
551
552#[derive(Debug, Clone)]
553struct EditorFactsProjection {
554 text_index: FenceTextIndex,
555 diagnostics: Vec<AnalysisRecoveryDiagnostic>,
556}
557
558impl EditorFactsProjection {
559 fn fallback(
560 source: &str,
561 diagram_type: Option<&str>,
562 diagnostics: Vec<AnalysisRecoveryDiagnostic>,
563 mode: AnalysisMode,
564 ) -> Self {
565 Self {
566 text_index: match mode {
567 AnalysisMode::Diagnostics => FenceTextIndex::default(),
568 AnalysisMode::RichFacts => FenceTextIndex::from_text(source, diagram_type),
569 },
570 diagnostics,
571 }
572 }
573}
574
575#[derive(Debug, Clone)]
576struct FlowchartFactsProjection {
577 facts: Option<AnalysisFlowchartFacts>,
578 diagnostics: Vec<AnalysisDiagnostic>,
579}
580
581pub fn engine_from_options(options: &AnalysisOptions) -> Engine {
582 let mut engine = Engine::new()
583 .with_fixed_today(options.fixed_today)
584 .with_fixed_local_offset_minutes(options.fixed_local_offset_minutes);
585
586 if let Some(site_config) = options.site_config.clone() {
587 engine = engine.with_site_config(site_config);
588 }
589
590 engine
591}
592
593#[cfg(test)]
594mod tests;