use std::path::Path;
use blue_lang_check::{Applicability, Diagnostic, Outcome};
use blue_lang_runtime::uses::{ResolvedProgram, SourceFile};
use blue_lang_syntax::Span;
use serde::Serialize;
#[derive(Serialize)]
pub struct Location {
pub file: Option<String>,
pub line: Option<usize>,
pub column: Option<usize>,
pub end_line: Option<usize>,
pub end_column: Option<usize>,
pub byte_start: Option<usize>,
pub byte_end: Option<usize>,
}
impl Location {
fn of(file: Option<&SourceFile>, span: Span) -> Self {
let name = file.map(|f| {
f.path
.as_ref()
.map_or_else(|| "<anonymous>".to_string(), |p| p.display().to_string())
});
let text = file.map(|f| f.text.as_str());
match text {
Some(t) if !span.is_synthetic() && span.end <= t.len() => {
let (line, column) = Span::line_col(t, span.start);
let (end_line, end_column) = Span::line_col(t, span.end);
Self {
file: name,
line: Some(line),
column: Some(column),
end_line: Some(end_line),
end_column: Some(end_column),
byte_start: Some(span.start),
byte_end: Some(span.end),
}
}
_ => Self {
file: name,
line: None,
column: None,
end_line: None,
end_column: None,
byte_start: None,
byte_end: None,
},
}
}
}
#[derive(Serialize)]
pub struct JsonRelated {
#[serde(flatten)]
pub at: Location,
pub message: String,
}
#[derive(Serialize)]
pub struct JsonEdit {
#[serde(flatten)]
pub at: Location,
pub original: String,
pub replacement: String,
}
#[derive(Serialize)]
pub struct JsonFix {
pub message: String,
pub applicability: &'static str,
pub edits: Vec<JsonEdit>,
}
#[derive(Serialize)]
pub struct JsonWaiver {
pub reason: String,
pub line: Option<usize>,
}
#[derive(Serialize)]
pub struct JsonDiagnostic {
pub code: &'static str,
pub slug: &'static str,
pub severity: &'static str,
pub message: String,
#[serde(flatten)]
pub at: Location,
pub help: Option<String>,
pub related: Vec<JsonRelated>,
pub fixes: Vec<JsonFix>,
pub waiver: Option<JsonWaiver>,
}
fn file_of(program: &ResolvedProgram, top_level: usize) -> Option<&SourceFile> {
program.owner_of(top_level).and_then(|id| program.file(id))
}
#[must_use]
pub fn to_json(
program: &ResolvedProgram,
d: &Diagnostic,
waiver: Option<JsonWaiver>,
) -> JsonDiagnostic {
let file = file_of(program, d.top_level);
let rule = d.code.rule();
JsonDiagnostic {
code: d.code.as_str(),
slug: rule.slug,
severity: d.severity.label(),
message: d.message.clone(),
at: Location::of(file, d.span),
help: d.help.clone(),
related: d
.related
.iter()
.map(|r| JsonRelated {
at: Location::of(file, r.span),
message: r.message.clone(),
})
.collect(),
fixes: d
.fixes
.iter()
.map(|f| JsonFix {
message: f.message.clone(),
applicability: f.applicability.label(),
edits: f
.edits
.iter()
.map(|e| JsonEdit {
at: Location::of(file, e.span),
original: e.original.clone(),
replacement: e.replacement.clone(),
})
.collect(),
})
.collect(),
waiver,
}
}
pub fn json_lines(program: &ResolvedProgram, outcome: &Outcome) -> serde_json::Result<String> {
let mut out = String::new();
for d in &outcome.diagnostics {
out.push_str(&serde_json::to_string(&to_json(program, d, None))?);
out.push('\n');
}
for w in &outcome.waived {
let file = file_of(program, w.waiver.top_level);
let line = file.map(|f| Span::line_col(&f.text, w.waiver.span.start).0);
let waiver = JsonWaiver {
reason: w.waiver.reason.clone(),
line,
};
out.push_str(&serde_json::to_string(&to_json(
program,
&w.diagnostic,
Some(waiver),
))?);
out.push('\n');
}
Ok(out)
}
pub fn syntax_json(path: &Path, text: &str, d: &Diagnostic) -> serde_json::Result<String> {
let file = SourceFile {
id: ResolvedProgram::ENTRY,
path: Some(path.to_path_buf()),
package: None,
text: text.to_string(),
};
let rule = d.code.rule();
let j = JsonDiagnostic {
code: d.code.as_str(),
slug: rule.slug,
severity: d.severity.label(),
message: d.message.clone(),
at: Location::of(Some(&file), d.span),
help: d.help.clone(),
related: Vec::new(),
fixes: Vec::new(),
waiver: None,
};
let mut s = serde_json::to_string(&j)?;
s.push('\n');
Ok(s)
}
#[must_use]
pub fn apply_machine_fixes(
program: &ResolvedProgram,
outcome: &Outcome,
text: &str,
) -> (String, usize) {
let mut edits: Vec<(Span, &str)> = Vec::new();
let mut applied = 0;
for d in &outcome.diagnostics {
if program.owner_of(d.top_level) != Some(ResolvedProgram::ENTRY) {
continue;
}
for fix in d
.fixes
.iter()
.filter(|f| f.applicability == Applicability::MachineApplicable)
{
let fits = fix.edits.iter().all(|e| {
text.get(e.span.start..e.span.end) == Some(e.original.as_str())
&& !edits
.iter()
.any(|(s, _)| e.span.start < s.end && s.start < e.span.end)
});
if !fits {
continue;
}
edits.extend(fix.edits.iter().map(|e| (e.span, e.replacement.as_str())));
applied += 1;
break;
}
}
edits.sort_by_key(|(s, _)| std::cmp::Reverse(s.start));
let mut out = text.to_string();
for (span, replacement) in edits {
out.replace_range(span.start..span.end, replacement);
}
(out, applied)
}
#[derive(Serialize)]
struct TargetJson {
kind: &'static str,
namespace: Option<String>,
name: Option<String>,
key: Option<String>,
}
fn target_json(t: &blue_lang_check::names::Target) -> TargetJson {
use blue_lang_check::names::Target;
use blue_lang_check::Namespace;
let key = t.resolved_symbol();
match t {
Target::Local => TargetJson { kind: "local", namespace: None, name: None, key },
Target::Def(ns, n) => TargetJson {
kind: "def",
namespace: match ns {
Namespace::Bidama(p) => Some(p.clone()),
_ => None,
},
name: Some(n.clone()),
key,
},
Target::Builtin(n) => TargetJson { kind: "builtin", namespace: None, name: Some(n.clone()), key },
Target::Ambiguous(_) => TargetJson { kind: "ambiguous", namespace: None, name: None, key },
Target::Unbound => TargetJson { kind: "unbound", namespace: None, name: None, key },
}
}
#[derive(Serialize)]
struct ReferenceJson {
#[serde(flatten)]
at: Location,
top_level: usize,
written: String,
opaque: bool,
flat: TargetJson,
ns: TargetJson,
}
#[derive(Serialize)]
struct ImportJson {
#[serde(flatten)]
at: Location,
package: String,
names: Vec<String>,
}
#[derive(Serialize)]
struct ResolvedJson {
namespace: Option<String>,
flat: Vec<String>,
ns: Vec<String>,
references: Vec<ReferenceJson>,
imports: Vec<ImportJson>,
first_line: Option<usize>,
}
pub fn resolved_json(
checked: &blue_lang_runtime::pipeline::Checked,
resolved: &blue_lang_check::names::Resolved,
) -> Result<String, serde_json::Error> {
use blue_lang_check::names::Rule;
let program = &checked.program;
let entry = |i: usize| program.owner_of(i) == Some(ResolvedProgram::ENTRY);
let tree = |rule| -> Vec<String> {
resolved
.resolved_tree(program.forms(), rule)
.iter()
.enumerate()
.filter(|(i, _)| entry(*i))
.map(|(_, f)| f.to_sexp().to_string())
.collect()
};
let file = program.file(ResolvedProgram::ENTRY);
let out = ResolvedJson {
namespace: file.and_then(|f| f.package.clone()),
flat: tree(Rule::Flat),
ns: tree(Rule::Namespaced),
references: resolved
.references
.iter()
.filter(|r| entry(r.top_level))
.map(|r| ReferenceJson {
at: Location::of(file, r.span),
top_level: r.top_level,
written: r.written.clone(),
opaque: r.opaque,
flat: target_json(&r.flat),
ns: target_json(&r.ns),
})
.collect(),
imports: program
.imports()
.iter()
.filter(|(f, _)| *f == ResolvedProgram::ENTRY)
.map(|(_, u)| ImportJson {
at: Location::of(file, u.span),
package: u.package.clone(),
names: u.names.iter().map(|(n, _)| n.clone()).collect(),
})
.collect(),
first_line: program
.forms()
.iter()
.enumerate()
.find(|(i, _)| entry(*i))
.and_then(|(_, f)| Location::of(file, f.span).line),
};
serde_json::to_string(&out)
}
fn tier_word(t: blue_lang_check::names::Tier) -> &'static str {
use blue_lang_check::names::Tier;
match t {
Tier::Local => "local",
Tier::Own => "own",
Tier::Imported => "imported",
Tier::Builtin => "builtin",
}
}
fn namespace_word(n: &blue_lang_check::Namespace) -> String {
use blue_lang_check::Namespace;
match n {
Namespace::Bidama(p) => p.clone(),
Namespace::File(_) => "this file".to_string(),
other => other.to_string(),
}
}
#[derive(Serialize)]
struct ShadowedJson {
tier: &'static str,
namespace: String,
}
#[derive(Serialize)]
struct OverrideJson {
kind: &'static str,
name: String,
#[serde(flatten)]
at: Location,
tier: &'static str,
namespace: String,
shadowed: Vec<ShadowedJson>,
}
pub fn overrides_json(
checked: &blue_lang_runtime::pipeline::Checked,
resolved: &blue_lang_check::names::Resolved,
) -> Result<String, serde_json::Error> {
use blue_lang_check::names::Target;
let program = &checked.program;
let file = program.file(ResolvedProgram::ENTRY);
let mut out = String::new();
let mut seen = std::collections::BTreeSet::new();
for r in &resolved.references {
if r.shadowed.is_empty() || program.owner_of(r.top_level) != Some(ResolvedProgram::ENTRY) {
continue;
}
if !seen.insert((r.span.start, r.span.end, r.written.clone())) {
continue;
}
let Target::Def(ns, _) = &r.ns else { continue };
let (tier, namespace) = (tier_of_winner(checked, r), namespace_word(ns));
let line = OverrideJson {
kind: "override",
name: r.written.clone(),
at: Location::of(file, r.span),
tier,
namespace,
shadowed: r
.shadowed
.iter()
.map(|(t, n)| ShadowedJson {
tier: tier_word(*t),
namespace: namespace_word(n),
})
.collect(),
};
out.push_str(&serde_json::to_string(&line)?);
out.push('\n');
}
Ok(out)
}
fn tier_of_winner(
checked: &blue_lang_runtime::pipeline::Checked,
r: &blue_lang_check::names::Reference,
) -> &'static str {
let own = blue_lang_runtime::pipeline::namespace_of(&checked.program, r.top_level);
checked
.names
.tiers_of(&r.written, &own, r.top_level)
.into_iter()
.find(|(_, nss)| !nss.is_empty())
.map_or("builtin", |(t, _)| tier_word(t))
}