Emitters
Emitters format diagnostics for different output targets: terminal, JSON, and SARIF.
Emitter Trait
pub trait Emitter {
fn emit(&mut self, diagnostic: &Diagnostic, source: &str);
fn finish(&mut self);
}
Terminal Emitter
Human-readable output with colors:
error[E2001]: type mismatch
--> src/mainsi:10:15
|
10 | let x: int = "hello"
| --- ^^^^^^^ expected `int`, found `str`
| |
| expected due to this annotation
|
= help: consider using `int()` to convert
Implementation
pub struct TerminalEmitter {
writer: Box<dyn Write>,
colors: bool,
}
impl Emitter for TerminalEmitter {
fn emit(&mut self, diag: &Diagnostic, source: &str) {
// Header: error[E2001]: message
self.write_header(diag);
// Location: --> file:line:col
self.write_location(diag);
// Source snippet with annotations
self.write_snippet(diag, source);
// Help text
if let Some(help) = &diag.help {
self.write_help(help);
}
// Suggested fixes
for fix in &diag.fixes {
self.write_fix_suggestion(fix);
}
writeln!(self.writer).ok();
}
}
Color Scheme
impl TerminalEmitter {
fn severity_color(&self, severity: Severity) -> &'static str {
match severity {
Severity::Error => "\x1b[1;31m", // Bold red
Severity::Warning => "\x1b[1;33m", // Bold yellow
Severity::Info => "\x1b[1;36m", // Bold cyan
Severity::Hint => "\x1b[1;32m", // Bold green
}
}
}
JSON Emitter
Machine-readable JSON output:
{
"diagnostics": [
{
"code": "E2001",
"severity": "error",
"message": "type mismatch",
"file": "src/main.ori",
"line": 10,
"column": 15,
"labels": [
{
"span": { "start": 150, "end": 157 },
"message": "expected `int`, found `str`",
"style": "primary"
}
],
"fixes": [
{
"message": "convert using `int()`",
"edits": [
{ "span": { "start": 150, "end": 150 }, "newText": "int(" },
{ "span": { "start": 157, "end": 157 }, "newText": ")" }
],
"applicability": "MaybeIncorrect"
}
],
"help": "consider using `int()` to convert"
}
]
}
Implementation
pub struct JsonEmitter {
diagnostics: Vec<serde_json::Value>,
}
impl Emitter for JsonEmitter {
fn emit(&mut self, diag: &Diagnostic, _source: &str) {
self.diagnostics.push(json!({
"code": diag.code.to_string(),
"severity": diag.severity.to_string(),
"message": diag.message,
"span": {
"start": diag.span.start,
"end": diag.span.end,
},
"labels": diag.labels.iter().map(|l| json!({
"span": { "start": l.span.start, "end": l.span.end },
"message": l.message,
"style": l.style.to_string(),
}))collect::<Vec<_>>(),
"fixes": diag.fixes.iter().map(|f| json!({
"message": f.message,
"edits": f.edits.iter().map(|e| json!({
"span": { "start": e.span.start, "end": e.span.end },
"newText": e.new_text,
}))collect::<Vec<_>>(),
"applicability": f.applicability.to_string(),
}))collect::<Vec<_>>(),
"help": diag.help,
}));
}
fn finish(&mut self) {
println!("{}", serde_json::to_string_pretty(&json!({
"diagnostics": &self.diagnostics
})).unwrap());
}
}
SARIF Emitter
SARIF (Static Analysis Results Interchange Format) for static analysis tools:
{
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "oric",
"version": "0.1.0",
"rules": [
{
"id": "E2001",
"shortDescription": { "text": "Type mismatch" },
"helpUri": "https://ori-lang.org/errors/E2001"
}
]
}
},
"results": [
{
"ruleId": "E2001",
"level": "error",
"message": { "text": "expected `int`, found `str`" },
"locations": [
{
"physicalLocation": {
"artifactLocation": { "uri": "src/main.ori" },
"region": {
"startLine": 10,
"startColumn": 15,
"endLine": 10,
"endColumn": 22
}
}
}
],
"fixes": [
{
"description": { "text": "convert using `int()`" },
"artifactChanges": [
{
"artifactLocation": { "uri": "src/main.ori" },
"replacements": [
{
"deletedRegion": { "startLine": 10, "startColumn": 15, "endColumn": 15 },
"insertedContent": { "text": "int(" }
}
]
}
]
}
]
}
]
}
]
}
Implementation
pub struct SarifEmitter {
results: Vec<sarif::Result>,
rules: HashSet<ErrorCode>,
}
impl Emitter for SarifEmitter {
fn emit(&mut self, diag: &Diagnostic, source: &str) {
self.rules.insert(diag.code);
self.results.push(sarif::Result {
rule_id: diag.code.to_string(),
level: match diag.severity {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Info => "note",
Severity::Hint => "none",
}.into(),
message: sarif::Message { text: diag.message.clone() },
locations: vec![self.span_to_location(diag.span, source)],
fixes: diag.fixes.iter().map(|f| self.convert_fix(f)).collect(),
..Default::default()
});
}
fn finish(&mut self) {
let sarif = sarif::Sarif {
version: "2.1.0".into(),
runs: vec![sarif::Run {
tool: sarif::Tool {
driver: sarif::Driver {
name: "oric".into(),
version: env!("CARGO_PKG_VERSION").into(),
rules: self.build_rules(),
},
},
results: std::mem::take(&mut self.results),
}],
};
println!("{}", serde_json::to_string_pretty(&sarif).unwrap());
}
}
Choosing an Emitter
pub fn create_emitter(format: OutputFormat) -> Box<dyn Emitter> {
match format {
OutputFormat::Terminal => Box::new(TerminalEmitter::new(true)),
OutputFormat::Plain => Box::new(TerminalEmitter::new(false)),
OutputFormat::Json => Box::new(JsonEmitter::new()),
OutputFormat::Sarif => Box::new(SarifEmitter::new()),
}
}
CLI Usage
# Terminal (default)
ori check src/main.ori
# JSON
ori check --format=json src/main.ori
# SARIF
ori check --format=sarif src/main.ori > results.sarif