use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use regex::Regex;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
struct ClassRegistryFile {
classes: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct ParityTiersFile {
#[serde(default)]
tiers: Vec<ParityTier>,
}
#[derive(Debug, Deserialize)]
struct ParityTier {
id: String,
#[serde(default)]
description: String,
#[serde(default)]
classes: Vec<String>,
}
#[derive(Debug, Serialize)]
struct Report {
source_styler: String,
source_native_style: String,
source_native_render: String,
styler_apply_static_literals: usize,
native_style_literals: usize,
in_both: Vec<String>,
only_in_styler: Vec<String>,
only_in_native: Vec<String>,
registry_classes: Vec<String>,
registry_not_in_native: Vec<String>,
registry_not_in_styler_static: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
tiers: Vec<BTreeMap<String, serde_json::Value>>,
}
fn main() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut json_out: Option<PathBuf> = None;
let mut args = std::env::args().skip(1);
while let Some(a) = args.next() {
if a == "--json" || a == "-j" {
json_out = args.next().map(PathBuf::from);
}
}
let styler_path = manifest_dir.join("../crepuscularity-runtime/src/styler.rs");
let style_path = manifest_dir.join("src/style.rs");
let render_path = manifest_dir.join("src/render.rs");
let styler_src = std::fs::read_to_string(&styler_path).unwrap_or_else(|e| {
eprintln!("read {}: {e}", styler_path.display());
std::process::exit(1);
});
let style_src = std::fs::read_to_string(&style_path).unwrap_or_else(|e| {
eprintln!("read {}: {e}", style_path.display());
std::process::exit(1);
});
let render_src = std::fs::read_to_string(&render_path).unwrap_or_else(|e| {
eprintln!("read {}: {e}", render_path.display());
std::process::exit(1);
});
let styler_body = extract_fn_body_brace(&styler_src, "apply_static").unwrap_or_else(|| {
eprintln!("parity-report: could not find apply_static in styler.rs");
std::process::exit(1);
});
let mut native_literals = BTreeSet::new();
for name in [
"apply_layout_class",
"apply_text_class",
"apply_color_and_radius",
] {
if let Some(body) = extract_fn_until_next_fn(&style_src, name) {
native_literals.extend(extract_match_arm_classes_from_body(body));
}
}
if let Some(body) = extract_fn_until_next_fn(&style_src, "is_scroll_container") {
native_literals.extend(extract_quoted_tailwind_tokens(body));
}
for name in ["stack_axis", "parse_gap_spacing"] {
if let Some(body) = extract_fn_until_next_fn(&render_src, name) {
native_literals.extend(extract_quoted_tailwind_tokens(body));
}
}
let styler_literals: BTreeSet<String> = extract_match_arm_classes_from_body(styler_body);
let registry_path = manifest_dir.join("data/class_registry.toml");
let registry: BTreeSet<String> = std::fs::read_to_string(®istry_path)
.ok()
.and_then(|s| toml::from_str::<ClassRegistryFile>(&s).ok())
.map(|r| r.classes.into_iter().collect())
.unwrap_or_default();
let tiers_path = manifest_dir.join("data/parity_tiers.json");
let tiers_file: ParityTiersFile = std::fs::read_to_string(&tiers_path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or(ParityTiersFile { tiers: vec![] });
let only_in_styler: Vec<String> = styler_literals
.difference(&native_literals)
.cloned()
.collect();
let only_in_native: Vec<String> = native_literals
.difference(&styler_literals)
.cloned()
.collect();
let in_both: Vec<String> = styler_literals
.intersection(&native_literals)
.cloned()
.collect();
let registry_not_in_native: Vec<String> = registry
.iter()
.filter(|c| !native_covers_class(c.as_str(), &native_literals))
.cloned()
.collect();
let registry_not_in_styler_static: Vec<String> =
registry.difference(&styler_literals).cloned().collect();
let styler_refs: BTreeSet<&str> = styler_literals.iter().map(String::as_str).collect();
let tier_rows: Vec<BTreeMap<String, serde_json::Value>> = tiers_file
.tiers
.iter()
.map(|t| {
let set: BTreeSet<&str> = t.classes.iter().map(|s| s.as_str()).collect();
let n_styler = set.intersection(&styler_refs).count();
let n_native = t
.classes
.iter()
.filter(|c| native_covers_class(c.as_str(), &native_literals))
.count();
let mut m = BTreeMap::new();
m.insert("id".into(), serde_json::json!(t.id));
m.insert("description".into(), serde_json::json!(t.description));
m.insert("classes_total".into(), serde_json::json!(t.classes.len()));
m.insert("in_styler_static".into(), serde_json::json!(n_styler));
m.insert("in_native_style".into(), serde_json::json!(n_native));
m
})
.collect();
let report = Report {
source_styler: styler_path.display().to_string(),
source_native_style: style_path.display().to_string(),
source_native_render: render_path.display().to_string(),
styler_apply_static_literals: styler_literals.len(),
native_style_literals: native_literals.len(),
in_both,
only_in_styler,
only_in_native,
registry_classes: registry.iter().cloned().collect(),
registry_not_in_native,
registry_not_in_styler_static,
tiers: tier_rows,
};
println!(
"GPUI styler apply_static: {} class literals",
report.styler_apply_static_literals
);
println!(
"Native style.rs match arms: {} class literals",
report.native_style_literals
);
println!("Intersection: {}", report.in_both.len());
println!("Only styler: {}", report.only_in_styler.len());
println!("Only native: {}", report.only_in_native.len());
if !report.registry_classes.is_empty() {
println!(
"Registry: {} classes (not in native: {}, not in styler static: {})",
report.registry_classes.len(),
report.registry_not_in_native.len(),
report.registry_not_in_styler_static.len()
);
}
let json = serde_json::to_string_pretty(&report).expect("serde_json::to_string_pretty(report)");
if let Some(p) = json_out {
std::fs::write(&p, &json).unwrap_or_else(|e| {
eprintln!("write {}: {e}", p.display());
std::process::exit(1);
});
eprintln!("wrote {}", p.display());
}
}
fn extract_fn_until_next_fn<'a>(src: &'a str, fn_name: &str) -> Option<&'a str> {
let needle = format!("fn {fn_name}");
let start = src.find(&needle)?;
let rest = &src[start..];
let end = rest[1..]
.find("\nfn ")
.map(|i| start + 1 + i)
.unwrap_or_else(|| src.len());
Some(&src[start..end])
}
fn extract_fn_body_brace<'a>(src: &'a str, fn_name: &str) -> Option<&'a str> {
let needle = format!("fn {fn_name}");
let start = src.find(&needle)?;
let brace = src[start..].find('{')?;
let open = start + brace;
let mut depth = 0i32;
for (i, ch) in src[open..].char_indices() {
match ch {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
let end = open + i + ch.len_utf8();
return Some(&src[open..end]);
}
}
_ => {}
}
}
None
}
fn extract_quoted_tailwind_tokens(body: &str) -> BTreeSet<String> {
let re = Regex::new(r#""([^"]+)""#).expect("regex");
let mut out = BTreeSet::new();
for cap in re.captures_iter(body) {
if let Some(s) = cap.get(1).map(|m| m.as_str()) {
if is_class_literal(s) {
out.insert(s.to_string());
}
}
}
out
}
fn extract_match_arm_classes_from_body(body: &str) -> BTreeSet<String> {
let re = Regex::new(r#""([^"]+)""#).expect("regex");
let lines: Vec<&str> = body.lines().collect();
let mut out = BTreeSet::new();
let mut i = 0usize;
while i < lines.len() {
let line = lines[i];
let t = line.trim_start();
let starts_arm =
t.starts_with('"') || t.starts_with('|') || (t.starts_with('_') && line.contains("=>"));
if starts_arm {
let start_line = i;
let mut chunk = String::new();
let mut found_arrow = false;
while i < lines.len() {
chunk.push_str(lines[i]);
chunk.push('\n');
if lines[i].contains("=>") {
found_arrow = true;
i += 1;
break;
}
i += 1;
}
if found_arrow {
let left = chunk.split_once("=>").map(|(l, _)| l).unwrap_or("");
for cap in re.captures_iter(left) {
if let Some(s) = cap.get(1).map(|m| m.as_str()) {
if is_class_literal(s) {
out.insert(s.to_string());
}
}
}
} else {
i = start_line + 1;
continue;
}
continue;
}
i += 1;
}
out
}
fn native_covers_class(class: &str, native: &BTreeSet<String>) -> bool {
if native.contains(class) {
return true;
}
native.contains("gap-")
&& class.starts_with("gap-")
&& class.len() > 4
&& class[4..].chars().all(|c| c.is_ascii_digit())
}
fn is_class_literal(s: &str) -> bool {
if s.is_empty() || s.len() > 96 {
return false;
}
if s == "_" || s.starts_with("//") {
return false;
}
if s.contains('{') || s.contains('}') {
return false;
}
if s.contains(' ') {
return false;
}
if s.starts_with('#') && s.len() == 7 {
return false;
}
true
}