use std::path::Path;
use blue_lang_check::project::Finding;
use blue_lang_check::{Diagnostic, Fix, Outcome};
use blue_lang_pkg::project_rules::{Failure, Outcome as ProjectOutcome};
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: String,
pub slug: String,
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().to_string(),
slug: rule.slug.to_string(),
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: json_fixes(file, &d.fixes),
waiver,
}
}
fn json_fixes(file: Option<&SourceFile>, fixes: &[Fix]) -> Vec<JsonFix> {
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()
}
#[must_use]
pub fn finding_json(
program: &ResolvedProgram,
f: &Finding,
waiver: Option<JsonWaiver>,
) -> JsonDiagnostic {
let file = file_of(program, f.top_level);
JsonDiagnostic {
code: f.code.to_string(),
slug: f.slug(),
severity: f.severity.label(),
message: f.message.clone(),
at: Location::of(file, f.span),
help: f.help.clone(),
related: Vec::new(),
fixes: json_fixes(file, &f.fixes),
waiver,
}
}
#[must_use]
pub fn failure_message(f: &Failure) -> String {
format!(
"project rule `{}` ({}) failed: {}",
f.rule,
f.source.display(),
f.failure
)
}
fn failure_json(program: &ResolvedProgram, f: &Failure) -> JsonDiagnostic {
let (file, span) = match f.top_level {
Some(i) => (
file_of(program, i),
program
.forms()
.get(i)
.map_or(Span::synthetic(), |form| form.span),
),
None => (None, Span::synthetic()),
};
JsonDiagnostic {
code: f.code.to_string(),
slug: "rule-failure".to_string(),
severity: "error",
message: failure_message(f),
at: Location::of(file, span),
help: None,
related: Vec::new(),
fixes: Vec::new(),
waiver: None,
}
}
pub fn project_json_lines(
program: &ResolvedProgram,
project: &ProjectOutcome,
) -> serde_json::Result<String> {
let mut out = String::new();
let mut line = |j: JsonDiagnostic| -> serde_json::Result<()> {
out.push_str(&serde_json::to_string(&j)?);
out.push('\n');
Ok(())
};
for f in &project.findings {
line(finding_json(program, f, None))?;
}
for w in &project.waived {
let file = file_of(program, w.waiver.top_level);
let waiver = JsonWaiver {
reason: w.waiver.reason.clone(),
line: file.map(|f| Span::line_col(&f.text, w.waiver.span.start).0),
};
line(finding_json(program, &w.finding, Some(waiver)))?;
}
for d in &project.diagnostics {
line(to_json(program, d, None))?;
}
for f in &project.failures {
line(failure_json(program, f))?;
}
Ok(out)
}
#[must_use]
pub fn render_finding(program: &ResolvedProgram, f: &Finding) -> String {
let head = f.to_string();
let mut out = program.locate(f.top_level, f.span, &head).to_string();
if let Some(help) = &f.help {
out.push_str("\n help: ");
out.push_str(help);
}
out
}
#[must_use]
pub fn render_failure(program: &ResolvedProgram, f: &Failure) -> String {
let head = format!("error[{}]: {}", f.code, failure_message(f));
match f.top_level {
Some(i) => {
let span = program
.forms()
.get(i)
.map_or(Span::synthetic(), |form| form.span);
program.locate(i, span, &head).to_string()
}
None => head,
}
}
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().to_string(),
slug: rule.slug.to_string(),
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,
project: &ProjectOutcome,
text: &str,
) -> (String, usize) {
blue_lang_check::fixes::apply_fixes(
outcome
.diagnostics
.iter()
.map(|d| (d.top_level, d.fixes.as_slice()))
.chain(
project
.findings
.iter()
.map(|f| (f.top_level, f.fixes.as_slice())),
),
&|i| program.owner_of(i) == Some(ResolvedProgram::ENTRY),
text,
)
}
#[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,
locals: Vec<String>,
position: &'static str,
argc: Option<usize>,
nullary: bool,
}
#[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>,
definitions: Vec<DefinitionJson>,
builtins: Vec<String>,
reserved: Vec<&'static str>,
}
#[derive(Serialize)]
struct DefinitionJson {
#[serde(flatten)]
at: Location,
name: String,
}
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),
locals: r.locals.clone(),
position: match r.position {
blue_lang_check::names::Position::Value => "value",
blue_lang_check::names::Position::Call(_) => "call",
blue_lang_check::names::Position::Place => "place",
blue_lang_check::names::Position::Syntax => "syntax",
},
argc: match r.position {
blue_lang_check::names::Position::Call(n) => Some(n),
_ => None,
},
nullary: r.nullary_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),
definitions: program
.forms()
.iter()
.enumerate()
.filter(|(i, _)| entry(*i))
.flat_map(|(_, f)| blue_lang_check::names::definitions_of(f))
.map(|(name, span, _)| DefinitionJson {
at: Location::of(file, span),
name,
})
.collect(),
builtins: {
let mut b: Vec<String> = checked
.names
.scopes()
.iter()
.filter(|s| {
!matches!(
s.namespace,
blue_lang_check::Namespace::File(_) | blue_lang_check::Namespace::Bidama(_)
)
})
.flat_map(|s| s.bindings().map(|b| b.name.clone()))
.collect();
b.sort();
b.dedup();
b
},
reserved: blue_lang_syntax::SURFACE_KEYWORDS
.iter()
.chain(blue_lang_syntax::BLOCK_KEYWORDS)
.copied()
.collect(),
};
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))
}