1use std::collections::{BTreeMap, BTreeSet};
7use std::env;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11pub const DEFAULT_DAP_FEATURES: &[&str] =
13 &["dap.breakpoints.basic", "dap.core", "dap.inline_values"];
14
15#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
17pub struct Meta {
18 pub version: String,
20 pub lsp_version: String,
22 #[serde(default)]
24 pub compliance_percent: Option<u32>,
25}
26
27#[derive(
29 Debug, Clone, Copy, serde::Deserialize, serde::Serialize, PartialEq, Eq, PartialOrd, Ord, Hash,
30)]
31#[serde(rename_all = "lowercase")]
32pub enum Maturity {
33 Experimental,
35 Preview,
37 Ga,
39 Planned,
41 Production,
43}
44
45impl Maturity {
46 pub const fn is_advertised(self) -> bool {
48 matches!(self, Self::Ga | Self::Production)
49 }
50
51 pub const fn is_trackable(self) -> bool {
53 !matches!(self, Self::Planned)
54 }
55
56 pub const fn label(self) -> &'static str {
58 match self {
59 Self::Experimental => "experimental",
60 Self::Preview => "preview",
61 Self::Ga => "ga",
62 Self::Planned => "planned",
63 Self::Production => "production",
64 }
65 }
66}
67
68#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
70pub struct Feature {
71 pub id: String,
73 #[serde(default)]
75 pub spec: String,
76 #[serde(default)]
78 pub area: String,
79 pub maturity: Maturity,
81 #[serde(default)]
83 pub advertised: bool,
84 #[serde(default)]
86 pub tests: Vec<String>,
87 #[serde(default = "default_counts_in_coverage")]
89 pub counts_in_coverage: bool,
90 #[serde(default)]
92 pub description: String,
93}
94
95const fn default_counts_in_coverage() -> bool {
96 true
97}
98
99#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
101pub struct Catalog {
102 pub meta: Meta,
104 pub feature: Vec<Feature>,
106}
107
108impl Catalog {
109 pub fn features(&self) -> &[Feature] {
111 &self.feature
112 }
113
114 pub fn advertised_feature_ids(&self) -> Vec<&str> {
116 let mut ids = self
117 .feature
118 .iter()
119 .filter(|feature| feature.advertised && feature.maturity.is_advertised())
120 .map(|feature| feature.id.as_str())
121 .collect::<Vec<_>>();
122 ids.sort_unstable();
123 ids
124 }
125
126 pub fn area_feature_ids(&self, area: &str) -> Vec<&str> {
128 let mut ids: Vec<&str> = self
129 .feature
130 .iter()
131 .filter(|feature| feature.area == area)
132 .map(|feature| feature.id.as_str())
133 .collect();
134 ids.sort_unstable();
135 ids
136 }
137
138 pub fn trackable_feature_count(&self) -> usize {
140 self.feature.iter().filter(|feature| feature.maturity.is_trackable()).count()
141 }
142
143 pub fn advertised_trackable_count(&self) -> usize {
145 self.feature
146 .iter()
147 .filter(|feature| feature.advertised && feature.maturity.is_advertised())
148 .count()
149 }
150
151 pub fn trackable_feature_count_for_grid(&self) -> usize {
154 self.feature
155 .iter()
156 .filter(|feature| feature.maturity.is_trackable() && feature.counts_in_coverage)
157 .count()
158 }
159
160 pub fn advertised_trackable_count_for_grid(&self) -> usize {
163 self.feature
164 .iter()
165 .filter(|feature| {
166 feature.advertised && feature.maturity.is_advertised() && feature.counts_in_coverage
167 })
168 .count()
169 }
170
171 pub fn compliance_percent_for_grid(&self) -> f32 {
173 let trackable = self.trackable_feature_count_for_grid();
174 if trackable == 0 {
175 return 0.0;
176 }
177 let advertised = self.advertised_trackable_count_for_grid();
178 (advertised as f64 / trackable as f64 * 100.0).round() as f32
179 }
180
181 pub fn compliance_percent(&self) -> f32 {
183 let trackable = self.trackable_feature_count();
184 if trackable == 0 {
185 return 0.0;
186 }
187 let advertised = self.advertised_trackable_count();
188 (advertised as f64 / trackable as f64 * 100.0).round() as f32
189 }
190
191 pub fn area_statistics(&self) -> BTreeMap<String, AreaStats> {
193 let mut stats: BTreeMap<String, AreaStats> = BTreeMap::new();
194
195 for feature in &self.feature {
196 let entry = stats.entry(feature.area.clone()).or_default();
197 entry.total += 1;
198 if feature.advertised {
199 entry.advertised += 1;
200 }
201
202 match feature.maturity {
203 Maturity::Ga => entry.ga += 1,
204 Maturity::Production => entry.production += 1,
205 Maturity::Preview => entry.preview += 1,
206 Maturity::Experimental => entry.experimental += 1,
207 Maturity::Planned => entry.planned += 1,
208 }
209 }
210
211 stats
212 }
213
214 pub fn validate(&self) -> Result<(), CatalogError> {
216 let mut seen = BTreeSet::new();
217 let mut issues = Vec::new();
218
219 for feature in &self.feature {
220 if feature.id.trim().is_empty() {
221 issues.push("feature id must not be empty".to_string());
222 continue;
223 }
224 if !seen.insert(&feature.id) {
225 issues.push(format!("duplicate feature id: {}", feature.id));
226 }
227 }
228
229 if issues.is_empty() { Ok(()) } else { Err(CatalogError::Validation(issues.join(", "))) }
230 }
231}
232
233#[derive(Debug, Default, Clone, Copy)]
235pub struct AreaStats {
236 pub total: usize,
238 pub advertised: usize,
240 pub experimental: usize,
242 pub preview: usize,
244 pub ga: usize,
246 pub production: usize,
248 pub planned: usize,
250}
251
252impl AreaStats {
253 pub const fn trackable(&self) -> usize {
255 self.total - self.planned
256 }
257
258 pub fn coverage_percent(&self) -> u32 {
260 if self.total == 0 {
261 return 0;
262 }
263 ((self.advertised as f64 / self.total as f64) * 100.0).round() as u32
264 }
265
266 pub fn trackable_coverage_percent(&self) -> u32 {
268 let trackable = self.trackable();
269 if trackable == 0 {
270 return 0;
271 }
272 ((self.advertised as f64 / trackable as f64) * 100.0).round() as u32
273 }
274}
275
276#[derive(Debug, thiserror::Error)]
278pub enum CatalogError {
279 #[error("features catalog not found for manifest dir: {0}")]
281 MissingSource(PathBuf),
282
283 #[error("failed to read features catalog: {0}")]
285 Io(#[from] std::io::Error),
286
287 #[error("failed to parse features catalog: {0}")]
289 Parse(#[from] toml::de::Error),
290
291 #[error("invalid features catalog: {0}")]
293 Validation(String),
294}
295
296#[derive(Debug, Clone)]
298pub struct CatalogSource {
299 pub path: PathBuf,
301 pub kind: CatalogSourceKind,
303}
304
305impl CatalogSource {
306 pub const fn comment(&self) -> &'static str {
308 match self.kind {
309 CatalogSourceKind::Override => "// source: FEATURES_TOML_OVERRIDE\n",
310 CatalogSourceKind::Workspace => "// source: features.toml\n",
311 CatalogSourceKind::Vendored => "// source: features_sot.toml\n",
312 }
313 }
314}
315
316#[derive(Debug, Clone, Copy)]
318pub enum CatalogSourceKind {
319 Override,
321 Workspace,
323 Vendored,
325}
326
327pub fn resolve_catalog_source(manifest_dir: &Path) -> Result<CatalogSource, CatalogError> {
329 if let Ok(override_path) = env::var("FEATURES_TOML_OVERRIDE") {
330 let override_path = PathBuf::from(override_path);
331 if override_path.exists() {
332 return Ok(CatalogSource { path: override_path, kind: CatalogSourceKind::Override });
333 }
334 }
335
336 let local_workspace_candidate = manifest_dir.join("features.toml");
337 if local_workspace_candidate.exists() {
338 return Ok(CatalogSource {
339 path: local_workspace_candidate,
340 kind: CatalogSourceKind::Workspace,
341 });
342 }
343
344 let parent_workspace = manifest_dir.parent().and_then(Path::parent).and_then(|p| {
345 let path = p.join("features.toml");
346 path.exists().then_some(path)
347 });
348 if let Some(path) = parent_workspace {
349 return Ok(CatalogSource { path, kind: CatalogSourceKind::Workspace });
350 }
351
352 let vendored = manifest_dir.join("features_sot.toml");
353 if vendored.exists() {
354 return Ok(CatalogSource { path: vendored, kind: CatalogSourceKind::Vendored });
355 }
356
357 Err(CatalogError::MissingSource(manifest_dir.to_path_buf()))
358}
359
360pub fn read_catalog(path: &Path) -> Result<Catalog, CatalogError> {
362 let content = fs::read_to_string(path)?;
363 let catalog: Catalog = toml::from_str(&content)?;
364 catalog.validate()?;
365 Ok(catalog)
366}
367
368pub fn load_catalog_for_build(
370 manifest_dir: &Path,
371) -> Result<(Catalog, CatalogSource), CatalogError> {
372 let source = resolve_catalog_source(manifest_dir)?;
373 let catalog = read_catalog(&source.path)?;
374 Ok((catalog, source))
375}
376
377pub fn render_lsp_feature_catalog_module(catalog: &Catalog, source_comment: &str) -> String {
379 let mut sorted = catalog.feature.clone();
380 sorted.sort_by(|a, b| a.area.cmp(&b.area).then_with(|| a.id.cmp(&b.id)));
381
382 let advertised = catalog.advertised_feature_ids();
383
384 let mut code = String::new();
385 code.push_str("// @generated by build.rs; DO NOT EDIT.\n");
386 code.push_str(source_comment);
387 code.push('\n');
388
389 code.push_str("/// Current parser version extracted from features.toml metadata\n");
390 code.push_str(&format!("pub const VERSION: &str = {:?};\n", catalog.meta.version));
391 code.push_str("/// LSP protocol version supported by this parser implementation\n");
392 code.push_str(&format!("pub const LSP_VERSION: &str = {:?};\n", catalog.meta.lsp_version));
393 code.push_str("/// Compliance percentage of advertised GA features vs trackable features\n");
394 code.push_str(&format!(
395 "pub const COMPLIANCE_PERCENT: f32 = {:.2};\n\n",
396 catalog.compliance_percent()
397 ));
398
399 code.push_str(
400 "/// Represents a single LSP feature with its metadata and implementation status\n",
401 );
402 code.push_str("#[derive(Debug, Clone)]\n");
403 code.push_str("pub struct Feature {\n");
404 code.push_str(" /// Unique identifier for this feature\n");
405 code.push_str(" pub id: &'static str,\n");
406 code.push_str(" /// LSP specification reference\n");
407 code.push_str(" pub spec: &'static str,\n");
408 code.push_str(" /// Functional area for this feature\n");
409 code.push_str(" pub area: &'static str,\n");
410 code.push_str(
411 " /// Maturity level (`experimental`, `preview`, `ga`, `planned`, `production`)\n",
412 );
413 code.push_str(" pub maturity: &'static str,\n");
414 code.push_str(" /// Advertised feature flag\n");
415 code.push_str(" pub advertised: bool,\n");
416 code.push_str(" /// Human-readable description\n");
417 code.push_str(" pub description: &'static str,\n");
418 code.push_str(" /// Include this feature in coverage / compliance accounting\n");
419 code.push_str(" pub counts_in_coverage: bool,\n");
420 code.push_str(" /// Test cases validating the feature\n");
421 code.push_str(" pub tests: &'static [&'static str],\n");
422 code.push_str("}\n\n");
423
424 code.push_str(
425 "/// Comprehensive catalog of all LSP features with their implementation status\n",
426 );
427 code.push_str("pub const ALL_FEATURES: &[Feature] = &[\n");
428 for feature in sorted {
429 code.push_str(" Feature {\n");
430 code.push_str(&format!(" id: {:?},\n", feature.id));
431 code.push_str(&format!(" spec: {:?},\n", feature.spec));
432 code.push_str(&format!(" area: {:?},\n", feature.area));
433 code.push_str(&format!(" maturity: {:?},\n", feature.maturity.label()));
434 code.push_str(&format!(" advertised: {},\n", feature.advertised));
435 code.push_str(&format!(" description: {:?},\n", feature.description));
436 code.push_str(&format!(" counts_in_coverage: {},\n", feature.counts_in_coverage));
437 code.push_str(&format!(" tests: &{:?},\n", feature.tests));
438 code.push_str(" },\n");
439 }
440 code.push_str("];\n\n");
441
442 code.push_str("/// Advertised feature IDs (GA/production and `advertised = true`).\n");
443 code.push_str("pub const ADVERTISED_LSP_FEATURES: &[&str] = &[\n");
444 for id in &advertised {
445 code.push_str(&format!(" {:?},\n", id));
446 }
447 code.push_str("];\n\n");
448
449 code.push_str("/// Returns advertised feature IDs (GA/production and `advertised = true`).\n");
450 code.push_str("pub fn advertised_features() -> &'static [&'static str] {\n");
451 code.push_str(" ADVERTISED_LSP_FEATURES\n");
452 code.push_str("}\n\n");
453
454 code.push_str("/// Checks whether a feature is currently advertised.\n");
455 code.push_str("pub fn has_feature(id: &str) -> bool {\n");
456 code.push_str(" ADVERTISED_LSP_FEATURES.contains(&id)\n");
457 code.push_str("}\n\n");
458
459 code.push_str("/// Returns the current LSP compliance percentage as a float.\n");
460 code.push_str("pub fn compliance_percent() -> f32 {\n");
461 code.push_str(" COMPLIANCE_PERCENT\n");
462 code.push_str("}\n");
463
464 code
465}
466
467pub fn render_dap_feature_catalog_module(ids: &[&str]) -> String {
469 let mut sorted = ids.to_vec();
470 sorted.sort_unstable();
471 sorted.dedup();
472
473 let mut code = String::new();
474 code.push_str("// @generated by build.rs; DO NOT EDIT.\n\n");
475 code.push_str("pub const ADVERTISED_DAP_FEATURES: &[&str] = &[\n");
476 for id in &sorted {
477 code.push_str(&format!(" {:?},\n", id));
478 }
479 code.push_str("];\n\n");
480 code.push_str("pub fn advertised_features() -> &'static [&'static str] {\n");
481 code.push_str(" ADVERTISED_DAP_FEATURES\n");
482 code.push_str("}\n\n");
483 code.push_str("pub fn has_feature(id: &str) -> bool {\n");
484 code.push_str(" ADVERTISED_DAP_FEATURES.contains(&id)\n");
485 code.push_str("}\n");
486 code
487}
488
489pub fn render_dap_fallback_module(default_features: &[&str]) -> String {
491 render_dap_feature_catalog_module(default_features)
492}
493
494pub fn render_lsp_fallback_module() -> String {
496 let mut code = String::new();
497 code.push_str("// Auto-generated minimal catalog - features.toml not found\n\n");
498 code.push_str("pub struct Feature {\n");
499 code.push_str(" pub id: &'static str,\n");
500 code.push_str(" pub spec: &'static str,\n");
501 code.push_str(" pub area: &'static str,\n");
502 code.push_str(" pub maturity: &'static str,\n");
503 code.push_str(" pub advertised: bool,\n");
504 code.push_str(" pub description: &'static str,\n");
505 code.push_str(" pub counts_in_coverage: bool,\n");
506 code.push_str(" pub tests: &'static [&'static str],\n");
507 code.push_str("}\n");
508 code.push_str("pub const VERSION: &str = \"0.10.0\";\n");
509 code.push_str("pub const LSP_VERSION: &str = \"3.18\";\n");
510 code.push_str("pub const COMPLIANCE_PERCENT: f32 = 0.0;\n");
511 code.push_str("pub const ALL_FEATURES: &[Feature] = &[];\n");
512 code.push_str("pub const ADVERTISED_LSP_FEATURES: &[&str] = &[];\n");
513 code.push_str(
514 "pub fn advertised_features() -> &'static [&'static str] { ADVERTISED_LSP_FEATURES }\n",
515 );
516 code.push_str("pub fn has_feature(_id: &str) -> bool { false }\n");
517 code.push_str("pub fn compliance_percent() -> f32 { 0.0 }\n");
518 code
519}