use serde::Serialize;
use super::file::{FileAnalysisView, ScopeAnalysisView, StepAnalysisView};
#[derive(Serialize)]
struct SourceRecord<'a> {
loc: String,
source: u8,
syntax: &'a str,
pretty: String,
sym: String,
}
#[derive(Serialize)]
struct TargetRecord<'a> {
loc: String,
target: u8,
kind: &'a str,
pretty: String,
sym: String,
}
fn spec_symbol(spec: &str, anchor: &str) -> String {
format!("SPEC_{spec}_{anchor}")
}
pub fn to_searchfox_records(analysis: &FileAnalysisView) -> String {
let mut lines: Vec<String> = Vec::new();
for scope in &analysis.scopes {
emit_scope_records(scope, &mut lines);
}
lines.join("\n")
}
fn emit_scope_records(scope: &ScopeAnalysisView, lines: &mut Vec<String>) {
let sym = spec_symbol(&scope.spec, &scope.anchor);
let line1 = scope.line + 1;
let col = scope.col;
let source = SourceRecord {
loc: format!("{line1}:{col}"),
source: 1,
syntax: "use",
pretty: format!("spec {}#{}", scope.spec, scope.anchor),
sym: sym.clone(),
};
lines.push(serde_json::to_string(&source).unwrap());
let target = TargetRecord {
loc: format!("{line1}:{col}"),
target: 1,
kind: "use",
pretty: format!("{}#{}", scope.spec, scope.anchor),
sym: sym.clone(),
};
lines.push(serde_json::to_string(&target).unwrap());
for step in &scope.validations {
emit_step_records(step, &sym, lines);
}
}
fn emit_step_records(step: &StepAnalysisView, scope_sym: &str, lines: &mut Vec<String>) {
let line1 = step.line + 1;
let col = step.col;
let step_num = step
.step
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(".");
let (syntax, indicator) = match step.result.as_str() {
"exact" => ("def", "\u{2713}"),
"fuzzy" => ("def", "~"),
"mismatch" => ("type", "\u{2717}"),
"not_found" => ("type", "?"),
_ => ("use", ""),
};
let pretty = if step.spec_text.is_empty() {
format!("Step {step_num} [{indicator} {}]", step.result)
} else {
format!(
"Step {step_num} [{indicator} {}] spec: {}",
step.result, step.spec_text
)
};
let source = SourceRecord {
loc: format!("{line1}:{col}"),
source: 1,
syntax,
pretty,
sym: scope_sym.to_string(),
};
lines.push(serde_json::to_string(&source).unwrap());
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analyze::file::{
CoverageSummary, FileAnalysisView, ScopeAnalysisView, StepAnalysisView,
};
fn make_analysis() -> FileAnalysisView {
FileAnalysisView {
scopes: vec![ScopeAnalysisView {
spec: "HTML".to_string(),
anchor: "navigate".to_string(),
url: "https://html.spec.whatwg.org/#navigate".to_string(),
line: 0,
col: 0,
validations: vec![
StepAnalysisView {
line: 2,
col: 4,
step: vec![1],
comment_text: "Let csp be form-submission".to_string(),
result: "fuzzy".to_string(),
spec_text: "Let cspNavigationType be form-submission".to_string(),
},
StepAnalysisView {
line: 5,
col: 4,
step: vec![99],
comment_text: "Nonexistent".to_string(),
result: "not_found".to_string(),
spec_text: String::new(),
},
],
coverage: Some(CoverageSummary {
total: 3,
implemented: 1,
missing: vec![vec![2], vec![3]],
warnings: 1,
reordered: 0,
}),
}],
}
}
#[test]
fn generates_source_and_target_for_url() {
let output = to_searchfox_records(&make_analysis());
let records: Vec<serde_json::Value> = output
.lines()
.map(|l| serde_json::from_str(l).unwrap())
.collect();
assert_eq!(records[0]["source"], 1);
assert_eq!(records[0]["sym"], "SPEC_HTML_navigate");
assert!(records[0]["pretty"]
.as_str()
.unwrap()
.contains("spec HTML#navigate"));
assert_eq!(records[1]["target"], 1);
assert_eq!(records[1]["kind"], "use");
assert_eq!(records[1]["sym"], "SPEC_HTML_navigate");
}
#[test]
fn generates_step_records() {
let output = to_searchfox_records(&make_analysis());
let records: Vec<serde_json::Value> = output
.lines()
.map(|l| serde_json::from_str(l).unwrap())
.collect();
assert_eq!(records.len(), 4);
let step1 = &records[2];
assert_eq!(step1["syntax"], "def");
assert!(step1["pretty"].as_str().unwrap().contains("Step 1"));
assert!(step1["pretty"].as_str().unwrap().contains("fuzzy"));
let step2 = &records[3];
assert_eq!(step2["syntax"], "type");
assert!(step2["pretty"].as_str().unwrap().contains("Step 99"));
assert!(step2["pretty"].as_str().unwrap().contains("not_found"));
}
#[test]
fn line_numbers_are_1_based() {
let output = to_searchfox_records(&make_analysis());
let records: Vec<serde_json::Value> = output
.lines()
.map(|l| serde_json::from_str(l).unwrap())
.collect();
assert_eq!(records[0]["loc"], "1:0");
assert_eq!(records[2]["loc"], "3:4");
assert_eq!(records[3]["loc"], "6:4");
}
#[test]
fn spec_symbol_format() {
assert_eq!(spec_symbol("HTML", "navigate"), "SPEC_HTML_navigate");
assert_eq!(spec_symbol("DOM", "concept-tree"), "SPEC_DOM_concept-tree");
}
#[test]
fn empty_analysis_produces_no_output() {
let analysis = FileAnalysisView { scopes: vec![] };
assert!(to_searchfox_records(&analysis).is_empty());
}
}