1use crate::{
2 AnalysisDiagnostic, AnalysisStatus, DiagnosticCategory, DiagnosticFix, DiagnosticFixEdit,
3 DiagnosticSeverity, DiagnosticSpan, SourceMap,
4 source_directives::{
5 directive_keyword_spans, frontmatter_config_key_spans, init_directive_config_key_spans,
6 },
7};
8use merman_core::{
9 BLOCK_WIDTH_WARNING_RULE_ID, DiagramWarningFact, FLOWCHART_EXPLICIT_DIRECTION_WARNING_RULE_ID,
10 FLOWCHART_UNKNOWN_STYLE_TARGET_WARNING_RULE_ID, GIT_GRAPH_DUPLICATE_COMMIT_WARNING_RULE_ID,
11};
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use std::collections::{BTreeMap, BTreeSet};
15
16pub const PREFER_INIT_DIRECTIVE_RULE_ID: &str = "merman.authoring.config.prefer_init_directive";
17pub const PREFER_FRONTMATTER_CONFIG_RULE_ID: &str =
18 "merman.authoring.config.prefer_frontmatter_config";
19pub const DEPRECATED_FLOWCHART_HTML_LABELS_RULE_ID: &str =
20 "merman.compatibility.config.deprecated_flowchart_html_labels";
21pub const DEPRECATED_EXTERNAL_DIAGRAM_LOADING_RULE_ID: &str =
22 "merman.compatibility.config.deprecated_external_diagram_loading";
23pub const NO_DIAGRAM_RULE_ID: &str = "merman.parse.no_diagram";
24pub const DIAGRAM_PARSE_RULE_ID: &str = "merman.parse.diagram_parse";
25pub const UNSUPPORTED_DIAGRAM_RULE_ID: &str = "merman.compatibility.unsupported_diagram";
26pub const RECOVERED_EDITOR_FACTS_RULE_ID: &str = "merman.parse.recovered_editor_facts";
27pub const RESOURCE_LIMIT_RULE_ID: &str = "merman.resource.source_bytes_exceeded";
28pub const MALFORMED_FRONT_MATTER_RULE_ID: &str = "merman.config.malformed_front_matter";
29pub const INVALID_DIRECTIVE_JSON_RULE_ID: &str = "merman.config.invalid_directive_json";
30pub const INVALID_FRONT_MATTER_YAML_RULE_ID: &str = "merman.config.invalid_front_matter_yaml";
31pub const PANIC_RULE_ID: &str = "merman.internal.panic";
32pub const INTERNAL_RULE_REGISTRY_GAP_RULE_ID: &str = "merman.internal.rule_registry_gap";
33pub const FLOWCHART_FACTS_PROJECTION_RULE_ID: &str = "merman.internal.flowchart_facts_projection";
34pub const BLOCK_WIDTH_RULE_ID: &str = "merman.block.width_exceeds_columns";
35pub const FLOWCHART_EXPLICIT_DIRECTION_RULE_ID: &str =
36 "merman.authoring.flowchart.explicit_direction";
37pub const FLOWCHART_UNKNOWN_STYLE_TARGET_RULE_ID: &str =
38 "merman.semantic.flowchart.unknown_style_target";
39pub const GIT_GRAPH_DUPLICATE_COMMIT_RULE_ID: &str = "merman.git_graph.duplicate_commit_id";
40pub const RULE_CATALOG_RESPONSE_VERSION: u32 = 1;
41
42const DEPRECATED_FLOWCHART_HTML_LABELS_INIT_CONFIG_PATHS: [&[&str]; 1] =
43 [&["flowchart", "htmlLabels"]];
44const DEPRECATED_FLOWCHART_HTML_LABELS_FLOWCHART_INIT_WRAPPER_PATHS: [&[&str]; 2] = [
45 &["config", "htmlLabels"],
46 &["config", "flowchart", "htmlLabels"],
47];
48const DEPRECATED_FLOWCHART_HTML_LABELS_FRONTMATTER_CONFIG_PATHS: [&[&str]; 2] = [
49 &["flowchart", "htmlLabels"],
50 &["config", "flowchart", "htmlLabels"],
51];
52const DEPRECATED_EXTERNAL_DIAGRAM_LOADING_CONFIG_PATHS: [&[&str]; 2] =
53 [&["lazyLoadedDiagrams"], &["loadExternalDiagramsAtStartup"]];
54const DEPRECATED_EXTERNAL_DIAGRAM_LOADING_FRONTMATTER_CONFIG_PATHS: [&[&str]; 2] = [
55 &["config", "lazyLoadedDiagrams"],
56 &["config", "loadExternalDiagramsAtStartup"],
57];
58
59#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum AnalysisRuleProfile {
62 #[default]
63 Core,
64 Recommended,
65 Strict,
66}
67
68impl AnalysisRuleProfile {
69 pub const fn as_str(self) -> &'static str {
70 match self {
71 Self::Core => "core",
72 Self::Recommended => "recommended",
73 Self::Strict => "strict",
74 }
75 }
76
77 const fn includes(self, minimum: Self) -> bool {
78 self as u8 >= minimum as u8
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum RuleOrigin {
85 MermaidSyntax,
86 MermaidCompatibility,
87 MermanAuthoring,
88 MermanResourcePolicy,
89 MermanInternal,
90}
91
92impl RuleOrigin {
93 pub const fn as_str(self) -> &'static str {
94 match self {
95 Self::MermaidSyntax => "mermaid_syntax",
96 Self::MermaidCompatibility => "mermaid_compatibility",
97 Self::MermanAuthoring => "merman_authoring",
98 Self::MermanResourcePolicy => "merman_resource_policy",
99 Self::MermanInternal => "merman_internal",
100 }
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct RuleDescriptor {
106 pub id: &'static str,
107 pub description: &'static str,
108 pub evidence: &'static [&'static str],
109 pub default_severity: DiagnosticSeverity,
110 pub category: DiagnosticCategory,
111 pub default_enabled: bool,
112 pub default_profile: AnalysisRuleProfile,
113 pub origin: RuleOrigin,
114 pub fixable: bool,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
118pub struct RuleCatalogEntry {
119 pub id: &'static str,
120 pub description: &'static str,
121 pub evidence: &'static [&'static str],
122 pub default_severity: DiagnosticSeverity,
123 pub category: DiagnosticCategory,
124 pub default_enabled: bool,
125 pub default_profile: AnalysisRuleProfile,
126 pub origin: RuleOrigin,
127 pub configurable: bool,
128 pub fixable: bool,
129}
130
131impl RuleCatalogEntry {
132 fn from_descriptor(descriptor: RuleDescriptor) -> Self {
133 Self {
134 id: descriptor.id,
135 description: descriptor.description,
136 evidence: descriptor.evidence,
137 default_severity: descriptor.default_severity,
138 category: descriptor.category,
139 default_enabled: descriptor.default_enabled,
140 default_profile: descriptor.default_profile,
141 origin: descriptor.origin,
142 configurable: is_configurable_rule_descriptor(descriptor),
143 fixable: descriptor.fixable,
144 }
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
149pub struct RuleCatalogResponse {
150 pub version: u32,
151 pub rules: Vec<RuleCatalogEntry>,
152}
153
154impl RuleCatalogResponse {
155 pub fn from_rules(rules: Vec<RuleCatalogEntry>) -> Self {
156 Self {
157 version: RULE_CATALOG_RESPONSE_VERSION,
158 rules,
159 }
160 }
161
162 pub fn current() -> Self {
163 Self::from_rules(rule_catalog())
164 }
165
166 pub fn configurable() -> Self {
167 Self::from_rules(configurable_rule_catalog())
168 }
169}
170
171const PREFER_INIT_DIRECTIVE_RULE: RuleDescriptor = RuleDescriptor {
172 id: PREFER_INIT_DIRECTIVE_RULE_ID,
173 description: "Prefer the canonical `init` directive keyword over the accepted `initialize` alias.",
174 evidence: &[
175 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/utils.ts",
176 "docs/adr/0072-lint-rule-governance.md",
177 ],
178 default_severity: DiagnosticSeverity::Hint,
179 category: DiagnosticCategory::Config,
180 default_enabled: false,
181 default_profile: AnalysisRuleProfile::Recommended,
182 origin: RuleOrigin::MermanAuthoring,
183 fixable: true,
184};
185
186const PREFER_FRONTMATTER_CONFIG_RULE: RuleDescriptor = RuleDescriptor {
187 id: PREFER_FRONTMATTER_CONFIG_RULE_ID,
188 description: "Prefer diagram frontmatter `config` over Mermaid init directives.",
189 evidence: &[
190 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/docs/config/directives.md",
191 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/docs/config/configuration.md",
192 ],
193 default_severity: DiagnosticSeverity::Hint,
194 category: DiagnosticCategory::Config,
195 default_enabled: false,
196 default_profile: AnalysisRuleProfile::Recommended,
197 origin: RuleOrigin::MermanAuthoring,
198 fixable: true,
199};
200
201const DEPRECATED_FLOWCHART_HTML_LABELS_RULE: RuleDescriptor = RuleDescriptor {
202 id: DEPRECATED_FLOWCHART_HTML_LABELS_RULE_ID,
203 description: "Report deprecated `flowchart.htmlLabels` config and recommend the root-level `htmlLabels` option.",
204 evidence: &[
205 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/config.ts",
206 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/config.type.ts",
207 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/docs/config/directives.md",
208 ],
209 default_severity: DiagnosticSeverity::Warning,
210 category: DiagnosticCategory::Config,
211 default_enabled: true,
212 default_profile: AnalysisRuleProfile::Core,
213 origin: RuleOrigin::MermaidCompatibility,
214 fixable: false,
215};
216
217const DEPRECATED_EXTERNAL_DIAGRAM_LOADING_RULE: RuleDescriptor = RuleDescriptor {
218 id: DEPRECATED_EXTERNAL_DIAGRAM_LOADING_RULE_ID,
219 description: "Report deprecated external diagram loading config and recommend `registerExternalDiagrams`.",
220 evidence: &[
221 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/config.ts",
222 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/mermaid.ts",
223 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/mermaid.spec.ts",
224 ],
225 default_severity: DiagnosticSeverity::Warning,
226 category: DiagnosticCategory::Config,
227 default_enabled: true,
228 default_profile: AnalysisRuleProfile::Core,
229 origin: RuleOrigin::MermaidCompatibility,
230 fixable: false,
231};
232
233const NO_DIAGRAM_RULE: RuleDescriptor = RuleDescriptor {
234 id: NO_DIAGRAM_RULE_ID,
235 description: "Report input that does not contain a Mermaid diagram.",
236 evidence: &[
237 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/diagram-api/detectType.ts",
238 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/mermaid.spec.ts",
239 ],
240 default_severity: DiagnosticSeverity::Error,
241 category: DiagnosticCategory::Parse,
242 default_enabled: true,
243 default_profile: AnalysisRuleProfile::Core,
244 origin: RuleOrigin::MermaidSyntax,
245 fixable: false,
246};
247
248const DIAGRAM_PARSE_RULE: RuleDescriptor = RuleDescriptor {
249 id: DIAGRAM_PARSE_RULE_ID,
250 description: "Report Mermaid diagram syntax that the parser cannot accept.",
251 evidence: &[
252 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/mermaid.ts",
253 "docs/adr/0070-diagnostics-first-analysis-contract.md",
254 ],
255 default_severity: DiagnosticSeverity::Error,
256 category: DiagnosticCategory::Parse,
257 default_enabled: true,
258 default_profile: AnalysisRuleProfile::Core,
259 origin: RuleOrigin::MermaidSyntax,
260 fixable: false,
261};
262
263const UNSUPPORTED_DIAGRAM_RULE: RuleDescriptor = RuleDescriptor {
264 id: UNSUPPORTED_DIAGRAM_RULE_ID,
265 description: "Report Mermaid diagram types that are recognized but unavailable in this build.",
266 evidence: &[
267 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/diagram-api/detectType.ts",
268 "docs/release/PACKAGE_SURFACES.md",
269 ],
270 default_severity: DiagnosticSeverity::Error,
271 category: DiagnosticCategory::Compatibility,
272 default_enabled: true,
273 default_profile: AnalysisRuleProfile::Core,
274 origin: RuleOrigin::MermaidCompatibility,
275 fixable: false,
276};
277
278const RECOVERED_EDITOR_FACTS_RULE: RuleDescriptor = RuleDescriptor {
279 id: RECOVERED_EDITOR_FACTS_RULE_ID,
280 description: "Report parser recovery diagnostics emitted while producing editor semantic facts.",
281 evidence: &[
282 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/mermaid.ts",
283 "docs/adr/0070-diagnostics-first-analysis-contract.md",
284 ],
285 default_severity: DiagnosticSeverity::Warning,
286 category: DiagnosticCategory::Parse,
287 default_enabled: true,
288 default_profile: AnalysisRuleProfile::Core,
289 origin: RuleOrigin::MermaidSyntax,
290 fixable: false,
291};
292
293const RESOURCE_LIMIT_RULE: RuleDescriptor = RuleDescriptor {
294 id: RESOURCE_LIMIT_RULE_ID,
295 description: "Report Mermaid sources that exceed the configured analysis source byte budget.",
296 evidence: &[
297 "docs/adr/0070-diagnostics-first-analysis-contract.md",
298 "docs/bindings/OPTIONS_JSON.md",
299 ],
300 default_severity: DiagnosticSeverity::Error,
301 category: DiagnosticCategory::Resource,
302 default_enabled: true,
303 default_profile: AnalysisRuleProfile::Core,
304 origin: RuleOrigin::MermanResourcePolicy,
305 fixable: false,
306};
307
308const MALFORMED_FRONT_MATTER_RULE: RuleDescriptor = RuleDescriptor {
309 id: MALFORMED_FRONT_MATTER_RULE_ID,
310 description: "Report malformed YAML front matter blocks before diagram parsing.",
311 evidence: &[
312 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/diagram-api/frontmatter.ts",
313 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/diagram-api/frontmatter.spec.ts",
314 ],
315 default_severity: DiagnosticSeverity::Error,
316 category: DiagnosticCategory::Config,
317 default_enabled: true,
318 default_profile: AnalysisRuleProfile::Core,
319 origin: RuleOrigin::MermaidSyntax,
320 fixable: false,
321};
322
323const INVALID_DIRECTIVE_JSON_RULE: RuleDescriptor = RuleDescriptor {
324 id: INVALID_DIRECTIVE_JSON_RULE_ID,
325 description: "Report Mermaid directive blocks whose JSON payload cannot be parsed.",
326 evidence: &[
327 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/diagram-api/regexes.ts",
328 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/utils.ts",
329 ],
330 default_severity: DiagnosticSeverity::Error,
331 category: DiagnosticCategory::Config,
332 default_enabled: true,
333 default_profile: AnalysisRuleProfile::Core,
334 origin: RuleOrigin::MermaidSyntax,
335 fixable: false,
336};
337
338const INVALID_FRONT_MATTER_YAML_RULE: RuleDescriptor = RuleDescriptor {
339 id: INVALID_FRONT_MATTER_YAML_RULE_ID,
340 description: "Report Mermaid front matter whose YAML payload cannot be parsed.",
341 evidence: &[
342 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/diagram-api/frontmatter.ts",
343 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/diagram-api/frontmatter.spec.ts",
344 ],
345 default_severity: DiagnosticSeverity::Error,
346 category: DiagnosticCategory::Config,
347 default_enabled: true,
348 default_profile: AnalysisRuleProfile::Core,
349 origin: RuleOrigin::MermaidSyntax,
350 fixable: false,
351};
352
353const PANIC_RULE: RuleDescriptor = RuleDescriptor {
354 id: PANIC_RULE_ID,
355 description: "Report an internal panic caught while analyzing Mermaid source.",
356 evidence: &["docs/adr/0070-diagnostics-first-analysis-contract.md"],
357 default_severity: DiagnosticSeverity::Error,
358 category: DiagnosticCategory::Internal,
359 default_enabled: true,
360 default_profile: AnalysisRuleProfile::Core,
361 origin: RuleOrigin::MermanInternal,
362 fixable: false,
363};
364
365const INTERNAL_RULE_REGISTRY_GAP_RULE: RuleDescriptor = RuleDescriptor {
366 id: INTERNAL_RULE_REGISTRY_GAP_RULE_ID,
367 description: "Report an internal rule registry gap while projecting diagnostics.",
368 evidence: &["docs/adr/0072-lint-rule-governance.md"],
369 default_severity: DiagnosticSeverity::Error,
370 category: DiagnosticCategory::Internal,
371 default_enabled: true,
372 default_profile: AnalysisRuleProfile::Core,
373 origin: RuleOrigin::MermanInternal,
374 fixable: false,
375};
376
377const FLOWCHART_FACTS_PROJECTION_RULE: RuleDescriptor = RuleDescriptor {
378 id: FLOWCHART_FACTS_PROJECTION_RULE_ID,
379 description: "Report an internal failure while projecting flowchart parser model facts.",
380 evidence: &["docs/adr/0070-diagnostics-first-analysis-contract.md"],
381 default_severity: DiagnosticSeverity::Error,
382 category: DiagnosticCategory::Internal,
383 default_enabled: true,
384 default_profile: AnalysisRuleProfile::Core,
385 origin: RuleOrigin::MermanInternal,
386 fixable: false,
387};
388
389const BLOCK_WIDTH_RULE: RuleDescriptor = RuleDescriptor {
390 id: BLOCK_WIDTH_RULE_ID,
391 description: "Report block diagram entries that exceed the configured column width.",
392 evidence: &[
393 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/docs/syntax/block.md",
394 ],
395 default_severity: DiagnosticSeverity::Warning,
396 category: DiagnosticCategory::Semantic,
397 default_enabled: true,
398 default_profile: AnalysisRuleProfile::Core,
399 origin: RuleOrigin::MermaidCompatibility,
400 fixable: false,
401};
402const FLOWCHART_EXPLICIT_DIRECTION_RULE: RuleDescriptor = RuleDescriptor {
403 id: FLOWCHART_EXPLICIT_DIRECTION_RULE_ID,
404 description: "Recommend explicit flowchart header directions and offer an insertion quickfix.",
405 evidence: &[
406 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/docs/syntax/flowchart.md",
407 "docs/adr/0072-lint-rule-governance.md",
408 ],
409 default_severity: DiagnosticSeverity::Hint,
410 category: DiagnosticCategory::Semantic,
411 default_enabled: false,
412 default_profile: AnalysisRuleProfile::Recommended,
413 origin: RuleOrigin::MermanAuthoring,
414 fixable: true,
415};
416const FLOWCHART_UNKNOWN_STYLE_TARGET_RULE: RuleDescriptor = RuleDescriptor {
417 id: FLOWCHART_UNKNOWN_STYLE_TARGET_RULE_ID,
418 description: "Report flowchart `style` directives that would auto-create an unknown node target.",
419 evidence: &[
420 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/diagrams/flowchart/flowDb.ts",
421 ],
422 default_severity: DiagnosticSeverity::Warning,
423 category: DiagnosticCategory::Semantic,
424 default_enabled: true,
425 default_profile: AnalysisRuleProfile::Core,
426 origin: RuleOrigin::MermaidCompatibility,
427 fixable: false,
428};
429const GIT_GRAPH_DUPLICATE_COMMIT_RULE: RuleDescriptor = RuleDescriptor {
430 id: GIT_GRAPH_DUPLICATE_COMMIT_RULE_ID,
431 description: "Report duplicate gitGraph commit ids.",
432 evidence: &[
433 "https://github.com/mermaid-js/mermaid/blob/41646dfd43ac83f001b03c70605feb036afae46d/packages/mermaid/src/diagrams/git/gitGraphAst.ts",
434 ],
435 default_severity: DiagnosticSeverity::Warning,
436 category: DiagnosticCategory::Semantic,
437 default_enabled: true,
438 default_profile: AnalysisRuleProfile::Core,
439 origin: RuleOrigin::MermaidCompatibility,
440 fixable: false,
441};
442const RULE_DESCRIPTORS: &[RuleDescriptor] = &[
443 PREFER_INIT_DIRECTIVE_RULE,
444 PREFER_FRONTMATTER_CONFIG_RULE,
445 DEPRECATED_FLOWCHART_HTML_LABELS_RULE,
446 DEPRECATED_EXTERNAL_DIAGRAM_LOADING_RULE,
447 NO_DIAGRAM_RULE,
448 DIAGRAM_PARSE_RULE,
449 UNSUPPORTED_DIAGRAM_RULE,
450 RECOVERED_EDITOR_FACTS_RULE,
451 RESOURCE_LIMIT_RULE,
452 MALFORMED_FRONT_MATTER_RULE,
453 INVALID_DIRECTIVE_JSON_RULE,
454 INVALID_FRONT_MATTER_YAML_RULE,
455 PANIC_RULE,
456 INTERNAL_RULE_REGISTRY_GAP_RULE,
457 FLOWCHART_FACTS_PROJECTION_RULE,
458 BLOCK_WIDTH_RULE,
459 FLOWCHART_EXPLICIT_DIRECTION_RULE,
460 FLOWCHART_UNKNOWN_STYLE_TARGET_RULE,
461 GIT_GRAPH_DUPLICATE_COMMIT_RULE,
462];
463
464pub fn rule_descriptors() -> &'static [RuleDescriptor] {
465 RULE_DESCRIPTORS
466}
467
468pub fn rule_catalog() -> Vec<RuleCatalogEntry> {
469 RULE_DESCRIPTORS
470 .iter()
471 .copied()
472 .map(RuleCatalogEntry::from_descriptor)
473 .collect()
474}
475
476pub fn configurable_rule_catalog() -> Vec<RuleCatalogEntry> {
477 configurable_rule_descriptors()
478 .map(RuleCatalogEntry::from_descriptor)
479 .collect()
480}
481
482pub fn rule_catalog_response() -> RuleCatalogResponse {
483 RuleCatalogResponse::current()
484}
485
486pub fn configurable_rule_catalog_response() -> RuleCatalogResponse {
487 RuleCatalogResponse::configurable()
488}
489
490pub fn rule_catalog_response_json_bytes() -> Result<Vec<u8>, serde_json::Error> {
491 serde_json::to_vec(&rule_catalog_response())
492}
493
494pub fn configurable_rule_catalog_response_json_bytes() -> Result<Vec<u8>, serde_json::Error> {
495 serde_json::to_vec(&configurable_rule_catalog_response())
496}
497
498pub fn configurable_rule_descriptors() -> impl Iterator<Item = RuleDescriptor> {
499 RULE_DESCRIPTORS
500 .iter()
501 .copied()
502 .filter(|descriptor| is_configurable_rule_descriptor(*descriptor))
503}
504
505pub fn configurable_rule_descriptor(rule_id: &str) -> Option<RuleDescriptor> {
506 configurable_rule_descriptors().find(|descriptor| descriptor.id == rule_id)
507}
508
509pub fn rule_descriptor(rule_id: &str) -> Option<RuleDescriptor> {
510 RULE_DESCRIPTORS
511 .iter()
512 .copied()
513 .find(|descriptor| descriptor.id == rule_id)
514}
515
516fn is_configurable_rule_descriptor(descriptor: RuleDescriptor) -> bool {
517 !matches!(
518 descriptor.category,
519 DiagnosticCategory::Internal | DiagnosticCategory::Resource
520 )
521}
522
523#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
524pub struct AnalysisRuleConfig {
525 #[serde(default)]
526 profile: AnalysisRuleProfile,
527 #[serde(default)]
528 enabled_rules: BTreeSet<String>,
529 #[serde(default)]
530 disabled_rules: BTreeSet<String>,
531 #[serde(default)]
532 severity_overrides: BTreeMap<String, DiagnosticSeverity>,
533}
534
535impl AnalysisRuleConfig {
536 pub fn with_profile(mut self, profile: AnalysisRuleProfile) -> Self {
537 self.profile = profile;
538 self
539 }
540
541 pub fn profile(&self) -> AnalysisRuleProfile {
542 self.profile
543 }
544
545 pub fn with_rule_enabled(mut self, rule_id: impl Into<String>) -> Self {
546 self.enable_rule(rule_id);
547 self
548 }
549
550 pub fn with_rule_disabled(mut self, rule_id: impl Into<String>) -> Self {
551 self.disable_rule(rule_id);
552 self
553 }
554
555 pub fn with_rule_severity(
556 mut self,
557 rule_id: impl Into<String>,
558 severity: DiagnosticSeverity,
559 ) -> Self {
560 self.set_rule_severity(rule_id, severity);
561 self
562 }
563
564 pub fn set_profile(&mut self, profile: AnalysisRuleProfile) {
565 self.profile = profile;
566 }
567
568 pub fn enable_rule(&mut self, rule_id: impl Into<String>) {
569 self.enabled_rules.insert(rule_id.into());
570 }
571
572 pub fn disable_rule(&mut self, rule_id: impl Into<String>) {
573 self.disabled_rules.insert(rule_id.into());
574 }
575
576 pub fn set_rule_severity(&mut self, rule_id: impl Into<String>, severity: DiagnosticSeverity) {
577 self.severity_overrides.insert(rule_id.into(), severity);
578 }
579
580 pub fn is_rule_enabled(&self, descriptor: RuleDescriptor) -> bool {
581 if self.disabled_rules.contains(descriptor.id) {
582 return false;
583 }
584 if self.enabled_rules.contains(descriptor.id) {
585 return true;
586 }
587 descriptor.default_enabled || self.profile.includes(descriptor.default_profile)
588 }
589
590 pub fn severity_for(&self, descriptor: RuleDescriptor) -> DiagnosticSeverity {
591 self.severity_overrides
592 .get(descriptor.id)
593 .copied()
594 .unwrap_or(descriptor.default_severity)
595 }
596}
597
598pub(crate) fn source_lint_diagnostics(
599 source: &str,
600 source_map: &SourceMap,
601 rule_config: &AnalysisRuleConfig,
602) -> Vec<AnalysisDiagnostic> {
603 let mut diagnostics = init_directive_alias_diagnostics(source, source_map, rule_config);
604 diagnostics.extend(prefer_frontmatter_config_diagnostics(
605 source,
606 source_map,
607 rule_config,
608 ));
609 diagnostics.extend(deprecated_flowchart_html_labels_diagnostics(
610 source,
611 source_map,
612 rule_config,
613 &DEPRECATED_FLOWCHART_HTML_LABELS_INIT_CONFIG_PATHS,
614 &DEPRECATED_FLOWCHART_HTML_LABELS_FRONTMATTER_CONFIG_PATHS,
615 ));
616 diagnostics.extend(deprecated_external_diagram_loading_diagnostics(
617 source,
618 source_map,
619 rule_config,
620 ));
621 diagnostics
622}
623
624pub(crate) fn parsed_source_lint_diagnostics(
625 source: &str,
626 source_map: &SourceMap,
627 rule_config: &AnalysisRuleConfig,
628 diagram_type: &str,
629) -> Vec<AnalysisDiagnostic> {
630 if merman_core::diagram_type_family_kind(diagram_type) != Some("flowchart") {
631 return Vec::new();
632 }
633
634 deprecated_flowchart_html_labels_diagnostics(
635 source,
636 source_map,
637 rule_config,
638 &DEPRECATED_FLOWCHART_HTML_LABELS_FLOWCHART_INIT_WRAPPER_PATHS,
639 &[],
640 )
641}
642
643pub(crate) fn semantic_warning_diagnostics(
644 diagram_type: &str,
645 model: &Value,
646 source_map: &SourceMap,
647 rule_config: &AnalysisRuleConfig,
648) -> Vec<AnalysisDiagnostic> {
649 let span = source_map.whole_source_span().ok();
650 let Some(warning_facts) = model
651 .get("warningFacts")
652 .and_then(|value| serde_json::from_value::<Vec<DiagramWarningFact>>(value.clone()).ok())
653 else {
654 return Vec::new();
655 };
656
657 semantic_warning_fact_diagnostics(diagram_type, warning_facts, span, source_map, rule_config)
658}
659
660fn semantic_warning_fact_diagnostics(
661 diagram_type: &str,
662 warning_facts: Vec<DiagramWarningFact>,
663 fallback_span: Option<DiagnosticSpan>,
664 source_map: &SourceMap,
665 rule_config: &AnalysisRuleConfig,
666) -> Vec<AnalysisDiagnostic> {
667 let mut diagnostics = Vec::with_capacity(warning_facts.len());
668
669 for fact in warning_facts {
670 match warning_fact_rule_descriptor(&fact.rule_id) {
671 Some(descriptor) if rule_config.is_rule_enabled(descriptor) => {
672 diagnostics.push(warning_for_fact(
673 diagram_type,
674 fact,
675 fallback_span.clone(),
676 source_map,
677 descriptor,
678 rule_config,
679 ))
680 }
681 Some(_) => {}
682 None => diagnostics.push(
683 internal_rule_registry_gap_diagnostic(
684 format!(
685 "unknown warning fact rule id `{}`: {}",
686 fact.rule_id, fact.message
687 ),
688 fallback_span.clone(),
689 )
690 .with_diagram_type(diagram_type),
691 ),
692 }
693 }
694
695 diagnostics
696}
697
698fn warning_for_fact(
699 diagram_type: &str,
700 fact: DiagramWarningFact,
701 fallback_span: Option<DiagnosticSpan>,
702 source_map: &SourceMap,
703 descriptor: RuleDescriptor,
704 rule_config: &AnalysisRuleConfig,
705) -> AnalysisDiagnostic {
706 let span = warning_fact_span(&fact, source_map, fallback_span);
707 let fix = warning_fact_fix(&fact, descriptor, source_map);
708 let mut diagnostic = AnalysisDiagnostic::new(
709 descriptor.id,
710 rule_config.severity_for(descriptor),
711 descriptor.category,
712 fact.message,
713 )
714 .with_diagram_type(diagram_type);
715
716 if let Some(span) = span {
717 diagnostic = diagnostic.with_span(span);
718 }
719
720 if let Some(fix) = fix {
721 diagnostic = diagnostic.with_fix(fix);
722 }
723
724 diagnostic
725}
726
727fn warning_fact_span(
728 fact: &DiagramWarningFact,
729 source_map: &SourceMap,
730 fallback_span: Option<DiagnosticSpan>,
731) -> Option<DiagnosticSpan> {
732 fact.span
733 .and_then(|span| source_map.span(span.start, span.end).ok())
734 .or(fallback_span)
735}
736
737fn warning_fact_fix(
738 fact: &DiagramWarningFact,
739 descriptor: RuleDescriptor,
740 source_map: &SourceMap,
741) -> Option<DiagnosticFix> {
742 let fix_span = fact.fix_span.or(fact.span)?;
743 let fix_span = source_map.span(fix_span.start, fix_span.end).ok()?;
744 match descriptor.id {
745 FLOWCHART_EXPLICIT_DIRECTION_RULE_ID => Some(
746 DiagnosticFix::new(
747 "Insert `TB` into the flowchart header",
748 vec![DiagnosticFixEdit::new(fix_span, " TB")],
749 )
750 .preferred(),
751 ),
752 _ => None,
753 }
754}
755
756fn warning_fact_rule_descriptor(rule_id: &str) -> Option<RuleDescriptor> {
757 match rule_id {
758 BLOCK_WIDTH_WARNING_RULE_ID => Some(BLOCK_WIDTH_RULE),
759 FLOWCHART_EXPLICIT_DIRECTION_WARNING_RULE_ID => Some(FLOWCHART_EXPLICIT_DIRECTION_RULE),
760 FLOWCHART_UNKNOWN_STYLE_TARGET_WARNING_RULE_ID => Some(FLOWCHART_UNKNOWN_STYLE_TARGET_RULE),
761 GIT_GRAPH_DUPLICATE_COMMIT_WARNING_RULE_ID => Some(GIT_GRAPH_DUPLICATE_COMMIT_RULE),
762 _ => None,
763 }
764}
765
766pub(crate) fn internal_rule_registry_gap_diagnostic(
767 message: impl Into<String>,
768 span: Option<DiagnosticSpan>,
769) -> AnalysisDiagnostic {
770 let mut diagnostic = AnalysisDiagnostic::error(
771 INTERNAL_RULE_REGISTRY_GAP_RULE_ID,
772 DiagnosticCategory::Internal,
773 message,
774 )
775 .with_code(
776 AnalysisStatus::InternalError.code(),
777 AnalysisStatus::InternalError.code_name(),
778 );
779
780 if let Some(span) = span {
781 diagnostic = diagnostic.with_span(span);
782 }
783
784 diagnostic
785}
786
787fn init_directive_alias_diagnostics(
788 source: &str,
789 source_map: &SourceMap,
790 rule_config: &AnalysisRuleConfig,
791) -> Vec<AnalysisDiagnostic> {
792 if !rule_config.is_rule_enabled(PREFER_INIT_DIRECTIVE_RULE) {
793 return Vec::new();
794 }
795 if rule_config.is_rule_enabled(PREFER_FRONTMATTER_CONFIG_RULE) {
796 return Vec::new();
797 }
798 let severity = rule_config.severity_for(PREFER_INIT_DIRECTIVE_RULE);
799
800 directive_keyword_spans(source)
801 .into_iter()
802 .filter_map(|keyword| {
803 (source.get(keyword.start..keyword.end) == Some("initialize"))
804 .then_some(keyword)
805 })
806 .filter_map(|keyword| {
807 let span = source_map.span(keyword.start, keyword.end).ok()?;
808 Some(
809 AnalysisDiagnostic::new(
810 PREFER_INIT_DIRECTIVE_RULE.id,
811 severity,
812 PREFER_INIT_DIRECTIVE_RULE.category,
813 "prefer `init` directive keyword over the `initialize` alias",
814 )
815 .with_span(span.clone())
816 .with_help("`initialize` is accepted as an alias; `init` is the canonical Mermaid directive keyword.")
817 .with_fix(
818 DiagnosticFix::new(
819 "Replace `initialize` with `init`",
820 vec![DiagnosticFixEdit::new(span, "init")],
821 )
822 .preferred(),
823 ),
824 )
825 })
826 .collect()
827}
828
829fn prefer_frontmatter_config_diagnostics(
830 source: &str,
831 source_map: &SourceMap,
832 rule_config: &AnalysisRuleConfig,
833) -> Vec<AnalysisDiagnostic> {
834 if !rule_config.is_rule_enabled(PREFER_FRONTMATTER_CONFIG_RULE) {
835 return Vec::new();
836 }
837 let severity = rule_config.severity_for(PREFER_FRONTMATTER_CONFIG_RULE);
838 let fix = crate::source_config_rewrite::init_directives_to_frontmatter_fix(source, source_map);
839
840 directive_keyword_spans(source)
841 .into_iter()
842 .filter_map(|keyword| {
843 matches!(source.get(keyword.start..keyword.end), Some("init" | "initialize"))
844 .then_some(keyword)
845 })
846 .filter_map(|keyword| {
847 let span = source_map.span(keyword.start, keyword.end).ok()?;
848 let mut diagnostic = AnalysisDiagnostic::new(
849 PREFER_FRONTMATTER_CONFIG_RULE.id,
850 severity,
851 PREFER_FRONTMATTER_CONFIG_RULE.category,
852 "prefer frontmatter `config` over Mermaid init directives",
853 )
854 .with_span(span)
855 .with_help(
856 "Mermaid deprecated directives from v10.5.0; diagram authors should move configuration into the diagram frontmatter `config` block.",
857 );
858 if let Some(fix) = fix.clone() {
859 diagnostic = diagnostic.with_fix(fix);
860 }
861 Some(diagnostic)
862 })
863 .collect()
864}
865
866fn deprecated_flowchart_html_labels_diagnostics(
867 source: &str,
868 source_map: &SourceMap,
869 rule_config: &AnalysisRuleConfig,
870 init_matching_paths: &[&[&str]],
871 frontmatter_matching_paths: &[&[&str]],
872) -> Vec<AnalysisDiagnostic> {
873 config_key_diagnostics(
874 source,
875 source_map,
876 rule_config,
877 DEPRECATED_FLOWCHART_HTML_LABELS_RULE,
878 init_matching_paths,
879 frontmatter_matching_paths,
880 "`flowchart.htmlLabels` is deprecated; use root-level `htmlLabels` instead",
881 "Mermaid keeps `flowchart.htmlLabels` as a compatibility fallback, but root-level `htmlLabels` takes precedence.",
882 )
883}
884
885fn deprecated_external_diagram_loading_diagnostics(
886 source: &str,
887 source_map: &SourceMap,
888 rule_config: &AnalysisRuleConfig,
889) -> Vec<AnalysisDiagnostic> {
890 config_key_diagnostics(
891 source,
892 source_map,
893 rule_config,
894 DEPRECATED_EXTERNAL_DIAGRAM_LOADING_RULE,
895 &DEPRECATED_EXTERNAL_DIAGRAM_LOADING_CONFIG_PATHS,
896 &DEPRECATED_EXTERNAL_DIAGRAM_LOADING_FRONTMATTER_CONFIG_PATHS,
897 "deprecated external diagram loading config; use `registerExternalDiagrams` instead",
898 "Mermaid warns that `lazyLoadedDiagrams` and `loadExternalDiagramsAtStartup` are deprecated in favor of the `registerExternalDiagrams` API.",
899 )
900}
901
902fn config_key_diagnostics(
903 source: &str,
904 source_map: &SourceMap,
905 rule_config: &AnalysisRuleConfig,
906 descriptor: RuleDescriptor,
907 init_matching_paths: &[&[&str]],
908 frontmatter_matching_paths: &[&[&str]],
909 message: &'static str,
910 help: &'static str,
911) -> Vec<AnalysisDiagnostic> {
912 if !rule_config.is_rule_enabled(descriptor) {
913 return Vec::new();
914 }
915 let severity = rule_config.severity_for(descriptor);
916
917 let mut spans = init_directive_config_key_spans(source, init_matching_paths);
918 spans.extend(frontmatter_config_key_spans(
919 source,
920 frontmatter_matching_paths,
921 ));
922
923 spans
924 .into_iter()
925 .filter_map(|span| {
926 let span = source_map.span(span.start, span.end).ok()?;
927 Some(
928 AnalysisDiagnostic::new(descriptor.id, severity, descriptor.category, message)
929 .with_span(span)
930 .with_help(help),
931 )
932 })
933 .collect()
934}
935
936#[cfg(test)]
937mod tests;