use rowan::{TextRange, TextSize};
use serde::{Serialize, Serializer};
pub mod render;
pub mod sarif;
pub use render::render;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(transparent)]
pub struct DiagCode(pub &'static str);
impl DiagCode {
pub fn as_str(&self) -> &'static str {
self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub const NONE: DiagCode = DiagCode("");
}
macro_rules! diag_codes {
(@lint) => { None };
(@lint $lint:literal) => { Some($lint) };
(@allow) => { false };
(@allow allow) => { true };
(
$(
$(#[$catalog_doc:meta])*
$catalog:ident {
$(
$(#[$code_doc:meta])*
$konst:ident = $code:literal, $severity:ident,
$summary:literal $(, lint = $lint:literal $(, default = $default:ident)?)?;
)+
}
)+
) => {
impl DiagCode {
$($(
$(#[$code_doc])*
pub const $konst: DiagCode = DiagCode($code);
)+)+
}
$(
$(#[$catalog_doc])*
pub const $catalog: &[CatalogEntry] = &[
$(CatalogEntry {
code: DiagCode::$konst,
severity: Severity::$severity,
summary: $summary,
lint: diag_codes!(@lint $($lint)?),
allow_by_default: diag_codes!(@allow $($($default)?)?),
},)+
];
)+
pub const ALL_CATALOGS: &[(&str, &[CatalogEntry])] = &[
$((stringify!($catalog), $catalog),)+
];
#[cfg(test)]
const CODE_CONSTANT_NAMES: &[(&str, &str)] = &[
$($((stringify!($konst), $code),)+)+
];
};
}
pub const RETIRED_RIDL_CODES: &[u16] = &[146, 147, 148, 415];
pub const RETIRED_TYPL_CODES: &[u16] = &[223];
diag_codes! {
FORM_CATALOG {
FORM_001 = "FORM-001", Error,
"invalid character";
FORM_002 = "FORM-002", Error,
"unterminated string literal";
FORM_003 = "FORM-003", Error,
"unterminated regex literal";
FORM_004 = "FORM-004", Error,
"unterminated block comment";
FORM_005 = "FORM-005", Error,
"leading zeros in integer literal";
FORM_101 = "FORM-101", Error,
"expected a specific token, or a construct the grammar admits here";
FORM_102 = "FORM-102", Error,
"unexpected token";
FORM_103 = "FORM-103", Error,
"unclosed delimiter";
FORM_104 = "FORM-104", Error,
"missing `package` declaration";
FORM_105 = "FORM-105", Error,
"reserved word used as an identifier";
FORM_106 = "FORM-106", Error,
"unknown attribute key";
FORM_107 = "FORM-107", Error,
"attribute key not allowed on this declaration kind";
FORM_108 = "FORM-108", Error,
"duplicate attribute key in one block";
}
TYPL_CATALOG {
TYPL_001 = "TYPL-001", Error,
"more than one `package` declaration in a file";
TYPL_002 = "TYPL-002", Error,
"package name does not mirror the directory path";
TYPL_003 = "TYPL-003", Error,
"wildcard, relative, or re-exporting import";
TYPL_004 = "TYPL-004", Error,
"circular package imports";
TYPL_005 = "TYPL-005", Error,
"a public declaration exposes an `internal` type";
TYPL_006 = "TYPL-006", Error,
"conflicting imports without an alias";
TYPL_007 = "TYPL-007", Warning,
"unused import", lint = "unused-import";
TYPL_008 = "TYPL-008", Warning,
"import alias without an actual collision", lint = "unneeded-import-alias";
TYPL_009 = "TYPL-009", Error,
"duplicate definition of the same name in a package";
TYPL_010 = "TYPL-010", Error,
"package name is reserved for a package the compiler provides";
TYPL_011 = "TYPL-011", Error,
"type reference names no visible declaration";
TYPL_101 = "TYPL-101", Warning,
"`integer` without a range constraint", lint = "unbounded-integer";
TYPL_102 = "TYPL-102", Warning,
"`float` without both a range and a `step`", lint = "unbounded-float";
TYPL_103 = "TYPL-103", Warning,
"`string`/`bytes` without explicit bounds", lint = "unbounded-length";
TYPL_104 = "TYPL-104", Error,
"range `min > max`";
TYPL_105 = "TYPL-105", Error,
"`step` type mismatch, non-positive, or larger than the range";
TYPL_106 = "TYPL-106", Error,
"invalid regex syntax in `match` or a regex `const`";
TYPL_108 = "TYPL-108", Error,
"`const` value violates its declared type constraints";
TYPL_109 = "TYPL-109", Error,
"init `= value` incompatible with the type or field constraints";
TYPL_110 = "TYPL-110", Error,
"unknown or malformed UCUM unit expression";
TYPL_111 = "TYPL-111", Error,
"integer range bound outside the `int64` domain";
TYPL_115 = "TYPL-115", Info,
"type has no derivable init value and no declared `= value`", lint = "no-init-value";
TYPL_201 = "TYPL-201", Error,
"array without explicit bounds";
TYPL_202 = "TYPL-202", Error,
"map without explicit bounds";
TYPL_203 = "TYPL-203", Error,
"enum values not unique or not explicitly assigned";
TYPL_204 = "TYPL-204", Error,
"union arm with a primitive type";
TYPL_206 = "TYPL-206", Error,
"recursive composite reference, direct or transitive";
TYPL_207 = "TYPL-207", Error,
"enumset bit positions not unique";
TYPL_208 = "TYPL-208", Error,
"`string`/`bytes` used directly as a field type";
TYPL_209 = "TYPL-209", Error,
"map key is not a named string type or a primitive";
TYPL_210 = "TYPL-210", Error,
"field, arm, or enum value re-declared under a `reserved` entry";
TYPL_211 = "TYPL-211", Warning,
"duplicate `reserved` entry", lint = "duplicate-reserved";
TYPL_212 = "TYPL-212", Error,
"`error` modifier on a declaration other than `enum`, `struct`, `union`";
TYPL_213 = "TYPL-213", Error,
"union mixes error and non-error arms without the result-union shape";
TYPL_214 = "TYPL-214", Error,
"`error union` contains a non-error-typed arm";
TYPL_215 = "TYPL-215", Error,
"field name declared twice in one struct or one tuple";
TYPL_216 = "TYPL-216", Error,
"enum value name declared twice in one enum";
TYPL_217 = "TYPL-217", Error,
"union arm name declared twice in one union";
TYPL_218 = "TYPL-218", Error,
"enumset bit name declared twice in one enumset";
TYPL_219 = "TYPL-219", Error,
"enumset bit position is not a number with an integer value";
TYPL_220 = "TYPL-220", Error,
"regex pattern the Rust `regex` crate cannot compile";
TYPL_222 = "TYPL-222", Info,
"one field name used with different units", lint = "inconsistent-unit";
TYPL_224 = "TYPL-224", Info,
"duplicate declaration shape", lint = "duplicate-shape";
TYPL_301 = "TYPL-301", Error,
"stream type `<T>` outside interaction position";
TYPL_302 = "TYPL-302", Error,
"timing annotation or duration literal in a typl context";
TYPL_303 = "TYPL-303", Error,
"`require`/`ensure` attribute in a typl context";
TYPL_304 = "TYPL-304", Error,
"interaction declaration in a typl context";
TYPL_401 = "TYPL-401", Warning,
"doc link or `@see` target that does not resolve", lint = "broken-doc-link";
TYPL_404 = "TYPL-404", Warning,
"blank line between a doc comment and its carrier", lint = "detached-doc-comment";
TYPL_405 = "TYPL-405", Warning,
"`@deprecated` doc tag without a reason string", lint = "deprecated-without-reason";
TYPL_406 = "TYPL-406", Warning,
"item without a doc comment", lint = "missing-docs";
TYPL_407 = "TYPL-407", Warning,
"doc comment in a position that is not a carrier", lint = "misplaced-doc-comment";
TYPL_408 = "TYPL-408", Warning,
"doc tag other than `@see`, `@since`, `@deprecated` and `@labels`", lint = "unknown-doc-tag";
TYPL_409 = "TYPL-409", Warning,
"`@see` or `@since` with a missing or malformed value", lint = "malformed-doc-tag";
TYPL_410 = "TYPL-410", Warning,
"doc comment written as `/** */`", lint = "doc-comment-style", default = allow;
}
RIDL_CATALOG {
RIDL_100 = "RIDL-100", Warning,
"`signal` or `event` without a timing annotation", lint = "missing-timing";
RIDL_101 = "RIDL-101", Error,
"timing range `@[X..Y]` with `X > Y`";
RIDL_102 = "RIDL-102", Error,
"zero or negative timing duration";
RIDL_103 = "RIDL-103", Error,
"strict-periodic `@Xms` on a kind other than `signal`";
RIDL_104 = "RIDL-104", Error,
"explicit return type on a `command`";
RIDL_105 = "RIDL-105", Error,
"`query` returning `()`";
RIDL_106 = "RIDL-106", Error,
"timing annotation on `fixed`, or attribute block on `fixed`";
RIDL_107 = "RIDL-107", Error,
"type declaration inside an `interface` or `service` body";
RIDL_108 = "RIDL-108", Warning,
"degenerate timing range `@[X..X]`", lint = "degenerate-timing-range";
RIDL_109 = "RIDL-109", Error,
"signal payload has no derivable init and no `= value` override";
RIDL_110 = "RIDL-110", Error,
"signal `= value` init override violates the payload constraints";
RIDL_112 = "RIDL-112", Warning,
"`command` or `query` takes the default response bound", lint = "missing-response-bound";
RIDL_140 = "RIDL-140", Error,
"duplicate `service` name across the workspace";
RIDL_141 = "RIDL-141", Error,
"`service` names a type that is not an `interface`";
RIDL_143 = "RIDL-143", Error,
"`service` publishes an `internal` interface";
RIDL_144 = "RIDL-144", Error,
"duplicate member name across a service's interfaces";
RIDL_145 = "RIDL-145", Error,
"the same interface named twice in one service";
RIDL_149 = "RIDL-149", Error,
"two names in one scope collide after a pinned name transform";
RIDL_201 = "RIDL-201", Error,
"stream `<T>` on a `signal` or `event` payload";
RIDL_202 = "RIDL-202", Error,
"stream element type not a named type, `string`, or `bytes`";
RIDL_301 = "RIDL-301", Error,
"`require` or `ensure` on `signal`, `event`, or `fixed`";
RIDL_302 = "RIDL-302", Error,
"`ensure` on `command`";
RIDL_303 = "RIDL-303", Error,
"fallible query return with no success path";
RIDL_304 = "RIDL-304", Warning,
"`error`-typed or result-union parameter on a `command` or `query`",
lint = "error-typed-parameter";
RIDL_305 = "RIDL-305", Warning,
"`ensure` clause that never references `result`", lint = "ensure-without-result";
RIDL_306 = "RIDL-306", Error,
"`require`/`ensure` expression outside the guaranteed subset";
RIDL_307 = "RIDL-307", Warning,
"contract-error category name declared in an `error` enum",
lint = "contract-error-name-in-enum";
RIDL_308 = "RIDL-308", Warning,
"named result union in query return position", lint = "named-result-union-in-query";
RIDL_401 = "RIDL-401", Error,
"interaction re-declared under a `reserved` name";
RIDL_402 = "RIDL-402", Error,
"duplicate interaction name in one interaction body";
RIDL_403 = "RIDL-403", Error,
"behaviour, user-interaction, or architecture declaration in a ridl context";
RIDL_404 = "RIDL-404", Warning,
"query named like a mutation", lint = "query-named-like-mutation";
RIDL_405 = "RIDL-405", Info,
"one `error` type shared across unrelated failure domains", lint = "shared-error-type";
RIDL_406 = "RIDL-406", Info,
"payload struct re-declares envelope metadata", lint = "redeclared-envelope-metadata";
RIDL_407 = "RIDL-407", Warning,
"interaction, struct field, or union arm ordinal changed against the published \
baseline", lint = "ordinal-changed";
RIDL_408 = "RIDL-408", Error,
"interaction removed, its tombstone dropped or moved, or its retired name redeclared";
RIDL_409 = "RIDL-409", Error,
"live `interfaces.lock` entry with no declaration";
RIDL_410 = "RIDL-410", Error,
"`interfaces.lock` is malformed";
RIDL_411 = "RIDL-411", Error,
"provisional interface number refused at publication";
RIDL_412 = "RIDL-412", Error,
"published interface number dropped without a retired entry";
RIDL_413 = "RIDL-413", Error,
"parameter name declared twice in one parameter list";
RIDL_414 = "RIDL-414", Info,
"interface members form disconnected type-sharing groups", lint = "low-cohesion-interface";
}
RSDL_CATALOG {
RSDL_305 = "RSDL-305", Error,
"`instances` is not a parenthesised list of one or more camelCase names";
RSDL_306 = "RSDL-306", Error,
"duplicate instance name in one component";
RSDL_307 = "RSDL-307", Error,
"`Unit` written in source";
RSDL_308 = "RSDL-308", Error,
"a component requires an interface listed by a service it offers";
RSDL_309 = "RSDL-309", Error,
"the same service or interface on two lines of one component";
RSDL_310 = "RSDL-310", Error,
"an `offers` line names something that is not a service";
RSDL_311 = "RSDL-311", Error,
"a `requires` line names a service whose shape is a list of interfaces";
RSDL_312 = "RSDL-312", Error,
"a `requires` line names neither an interface nor an inline-shape service";
RSDL_313 = "RSDL-313", Error,
"`external` written with a value";
RSDL_403 = "RSDL-403", Error,
"a closure component requires an interface no closure service lists";
RSDL_408 = "RSDL-408", Error,
"an interface listed by two services of the closure";
RSDL_409 = "RSDL-409", Warning,
"a `requires` resolves to a redundant provider set", lint = "redundant-provider-set";
RSDL_502 = "RSDL-502", Error,
"two closure components offer one service";
RSDL_504 = "RSDL-504", Error,
"a member line names a service that a declared component offers";
RSDL_601 = "RSDL-601", Error,
"more than one `system` in the workspace";
RSDL_602 = "RSDL-602", Error,
"a `system` member line names neither a component nor a service";
RSDL_603 = "RSDL-603", Error,
"a name listed twice in one `system` body";
RSDL_604 = "RSDL-604", Error,
"a declaration of another profile in an `.rsdl` file";
RSDL_701 = "RSDL-701", Error,
"an instance of a closure component with no placement in a deployment";
RSDL_702 = "RSDL-702", Error,
"a placement line names something outside the closure, or nothing";
RSDL_704 = "RSDL-704", Error,
"a deployment is `for` no declared `system`";
RSDL_705 = "RSDL-705", Error,
"two machines with one name in one deployment";
RSDL_706 = "RSDL-706", Error,
"an instance placed twice in one deployment";
RSDL_707 = "RSDL-707", Error,
"an `external` machine lists an implemented component";
RSDL_708 = "RSDL-708", Error,
"two deployments with one name in the workspace";
RSDL_709 = "RSDL-709", Error,
"a `depth`, `slots` or `budget` value is not an integer within its range, or the key \
is written bare";
RSDL_804 = "RSDL-804", Warning,
"a backend key whose namespace no configured backend claims",
lint = "unclaimed-backend-key";
RSDL_805 = "RSDL-805", Warning,
"a declared `depth` is below `ceil(max / min)` for an event a covered link consumes",
lint = "depth-below-bound";
RSDL_806 = "RSDL-806", Warning,
"an event whose contract bound is not derivable is consumed by a link with no declared `depth`",
lint = "depth-underivable";
RSDL_901 = "RSDL-901", Error,
"a `PLATFORM` distribution requires into an `APPLICATION` distribution";
RSDL_903 = "RSDL-903", Error,
"a distribution member line names something outside the closure, or nothing";
RSDL_904 = "RSDL-904", Error,
"an implemented closure component in no distribution";
RSDL_905 = "RSDL-905", Error,
"a component listed by two distributions";
RSDL_906 = "RSDL-906", Error,
"a name listed twice in one distribution body";
RSDL_907 = "RSDL-907", Error,
"an `external` component listed by a distribution";
RSDL_908 = "RSDL-908", Error,
"`tier` value other than `PLATFORM` or `APPLICATION`";
}
MANI_CATALOG {
MANI_001 = "MANI-001", Error,
"invalid manifest TOML";
MANI_002 = "MANI-002", Error,
"manifest declares both `[package]` and `[workspace]`";
MANI_003 = "MANI-003", Error,
"manifest declares neither `[package]` nor `[workspace]`";
MANI_004 = "MANI-004", Error,
"nested workspace: a member manifest declares `[workspace]`";
MANI_005 = "MANI-005", Warning,
"unknown manifest key", lint = "unknown-manifest-key";
MANI_006 = "MANI-006", Error,
"invalid package name";
MANI_007 = "MANI-007", Error,
"invalid import URL";
MANI_008 = "MANI-008", Error,
"workspace member directory has no `ridl.toml`";
MANI_009 = "MANI-009", Error,
"invalid `[defaults]` timing value";
MANI_010 = "MANI-010", Warning,
"`[lints]` entry names no lint, or its value is not a level", lint = "unknown-lint";
MANI_011 = "MANI-011", Error,
"`[codegen] header-file` cannot be read or holds a control character";
MANI_012 = "MANI-012", Error,
"`[codegen] header-file` is set in a workspace member";
MANI_101 = "MANI-101", Error,
"remote import fetch failed";
MANI_102 = "MANI-102", Error,
"fetched content hash does not match the lockfile";
MANI_103 = "MANI-103", Error,
"`--frozen`: no lockfile entry for a remote import";
MANI_104 = "MANI-104", Error,
"`--frozen`: a lockfile-pinned import is not cached";
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum Severity {
Error,
Warning,
Info,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(transparent)]
pub struct FileId(u32);
impl FileId {
pub const DETACHED: FileId = FileId(u32::MAX);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct Span {
pub file: FileId,
#[serde(serialize_with = "serialize_text_range")]
pub range: TextRange,
}
fn serialize_text_range<S: Serializer>(
range: &TextRange,
serializer: S,
) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("TextRange", 2)?;
state.serialize_field("start", &u32::from(range.start()))?;
state.serialize_field("end", &u32::from(range.end()))?;
state.end()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Label {
pub span: Span,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct FixIt {
pub span: Span,
pub replacement: String,
pub label: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Diagnostic {
pub code: DiagCode,
pub severity: Severity,
pub message: String,
pub primary: Span,
pub labels: Vec<Label>,
pub fixits: Vec<FixIt>,
}
#[derive(Debug)]
struct SourceEntry {
path: String,
text: String,
}
#[derive(Debug, Default)]
pub struct SourceMap {
files: Vec<SourceEntry>,
}
impl SourceMap {
pub fn new() -> Self {
Self::default()
}
pub fn file_id(&mut self, path: &str, text: &str) -> FileId {
if let Some(index) = self.files.iter().position(|entry| entry.path == path) {
return FileId(index as u32);
}
let id = FileId(self.files.len() as u32);
self.files.push(SourceEntry {
path: path.to_string(),
text: text.to_string(),
});
id
}
pub fn path(&self, id: FileId) -> Option<&str> {
self.files
.get(id.0 as usize)
.map(|entry| entry.path.as_str())
}
pub fn text(&self, id: FileId) -> Option<&str> {
self.files
.get(id.0 as usize)
.map(|entry| entry.text.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct LineCol {
pub line: u32,
pub column: u32,
}
pub fn line_col(text: &str, offset: TextSize) -> LineCol {
let mut offset = usize::from(offset).min(text.len());
while !text.is_char_boundary(offset) {
offset -= 1;
}
let before = &text[..offset];
let line = before.matches('\n').count() as u32 + 1;
let column = match before.rfind('\n') {
Some(newline) => before[newline + 1..].chars().count(),
None => before.chars().count(),
} as u32
+ 1;
LineCol { line, column }
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct JsonSpan {
pub path: String,
pub start: LineCol,
pub end: LineCol,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct JsonFixIt {
pub label: String,
pub replacement: String,
pub span: JsonSpan,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct JsonLabel {
pub message: String,
pub span: JsonSpan,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct JsonDiagnostic {
pub code: String,
pub severity: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub lint: Option<String>,
pub message: String,
pub span: JsonSpan,
pub labels: Vec<JsonLabel>,
pub fixes: Vec<JsonFixIt>,
}
fn json_span(span: Span, sources: &SourceMap) -> JsonSpan {
let path = sources.path(span.file).unwrap_or("").to_string();
let text = sources.text(span.file).unwrap_or("");
JsonSpan {
path,
start: line_col(text, span.range.start()),
end: line_col(text, span.range.end()),
}
}
fn severity_name(severity: Severity) -> &'static str {
match severity {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Info => "info",
}
}
pub fn to_json(diagnostics: &[Diagnostic], sources: &SourceMap) -> Vec<JsonDiagnostic> {
diagnostics
.iter()
.map(|diagnostic| JsonDiagnostic {
code: diagnostic.code.0.to_string(),
severity: severity_name(diagnostic.severity).to_string(),
lint: crate::lint::lint_of(diagnostic.code)
.and_then(|entry| entry.lint)
.map(str::to_string),
message: diagnostic.message.clone(),
span: json_span(diagnostic.primary, sources),
labels: diagnostic
.labels
.iter()
.map(|label| JsonLabel {
message: label.message.clone(),
span: json_span(label.span, sources),
})
.collect(),
fixes: diagnostic
.fixits
.iter()
.map(|fixit| JsonFixIt {
label: fixit.label.clone(),
replacement: fixit.replacement.clone(),
span: json_span(fixit.span, sources),
})
.collect(),
})
.collect()
}
pub fn remap_diagnostics(
diagnostics: impl IntoIterator<Item = Diagnostic>,
render_ids: &[FileId],
) -> Vec<Diagnostic> {
let remap_file =
|file: FileId| -> FileId { render_ids.get(file.0 as usize).copied().unwrap_or(file) };
diagnostics
.into_iter()
.map(|mut diagnostic| {
diagnostic.primary.file = remap_file(diagnostic.primary.file);
for label in &mut diagnostic.labels {
label.span.file = remap_file(label.span.file);
}
for fixit in &mut diagnostic.fixits {
fixit.span.file = remap_file(fixit.span.file);
}
diagnostic
})
.collect()
}
#[derive(Debug, Clone, Copy)]
pub struct CatalogEntry {
pub code: DiagCode,
pub severity: Severity,
pub summary: &'static str,
pub lint: Option<&'static str>,
pub allow_by_default: bool,
}
pub fn house_style_message(raw: &str) -> String {
if let Some(token) = raw.strip_prefix("expected ")
&& let Some(glyph) = punctuation_glyph(token)
{
return format!("expected {glyph}");
}
raw.to_string()
}
fn punctuation_glyph(debug_name: &str) -> Option<&'static str> {
Some(match debug_name {
"Colon" => "`:`",
"Eq" => "`=`",
"Semicolon" => "`;`",
"Comma" => "`,`",
"LBracket" => "`[`",
"RBracket" => "`]`",
"LBrace" => "`{`",
"RBrace" => "`}`",
"LParen" => "`(`",
"RParen" => "`)`",
"DotDot" => "`..`",
"Dot" => "`.`",
"Question" => "`?`",
"At" => "`@`",
"Pipe" => "`|`",
"Gt" => "`>`",
_ => return None,
})
}
impl SourceMap {
pub fn iter_files(&self) -> impl Iterator<Item = (&str, &str)> {
self.files
.iter()
.map(|entry| (entry.path.as_str(), entry.text.as_str()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_map_interns_by_path() {
let mut map = SourceMap::new();
let a = map.file_id("a.typl", "type A: m");
let b = map.file_id("b.typl", "type B: s");
let a_again = map.file_id("a.typl", "type A: m");
assert_ne!(a, b, "distinct paths get distinct ids");
assert_eq!(a, a_again, "the same path interns to the same id");
}
#[test]
fn source_map_path_reverses_file_id() {
let mut map = SourceMap::new();
let a = map.file_id("a.typl", "type A: m");
let b = map.file_id("b.typl", "type B: s");
assert_eq!(map.path(a), Some("a.typl"));
assert_eq!(map.path(b), Some("b.typl"));
assert_eq!(
map.path(FileId::DETACHED),
None,
"a detached id has no path"
);
assert_eq!(
map.path(FileId(2)),
None,
"an id the map never issued has no path",
);
}
#[test]
fn remap_diagnostics_rewrites_package_relative_ids() {
let mut render = SourceMap::new();
let _other = render.file_id("other.typl", "");
let a = render.file_id("pkg/a.typl", "type A: m");
let b = render.file_id("pkg/b.typl", "type B: s");
let render_ids = vec![a, b];
let span = |file: FileId| Span {
file,
range: TextRange::default(),
};
let diagnostic = |file: FileId| Diagnostic {
code: DiagCode::TYPL_009,
severity: Severity::Error,
message: "duplicate declaration of `X`".to_string(),
primary: span(file),
labels: vec![Label {
span: span(file),
message: "first declared here".to_string(),
}],
fixits: Vec::new(),
};
let mut pass_map = SourceMap::new();
let pkg_a = pass_map.file_id("pkg/a.typl", "type A: m");
let pkg_b = pass_map.file_id("pkg/b.typl", "type B: s");
let remapped = remap_diagnostics(
vec![
diagnostic(pkg_a),
diagnostic(pkg_b),
diagnostic(FileId::DETACHED),
],
&render_ids,
);
assert_eq!(remapped[0].primary.file, a);
assert_eq!(remapped[0].labels[0].span.file, a);
assert_eq!(remapped[1].primary.file, b);
assert_eq!(
remapped[2].primary.file,
FileId::DETACHED,
"a detached diagnostic stays detached",
);
}
#[test]
fn house_style_rewrites_debug_token_names() {
assert_eq!(house_style_message("expected RBracket"), "expected `]`");
assert_eq!(house_style_message("expected Colon"), "expected `:`");
assert_eq!(house_style_message("expected Eq"), "expected `=`");
assert_eq!(house_style_message("expected Semicolon"), "expected `;`");
assert_eq!(house_style_message("expected RParen"), "expected `)`");
assert_eq!(house_style_message("expected DotDot"), "expected `..`");
assert_eq!(house_style_message("expected Gt"), "expected `>`");
}
#[test]
fn every_expectable_token_has_a_glyph() {
const CALL: &str = ".expect(SyntaxKind::";
let root = workspace_root();
let mut sources = Vec::new();
collect_rust_sources(&root, &mut sources);
assert!(
sources.len() >= 60,
"the walk found only {} `.rs` files under {} — it is not reaching \
the workspace",
sources.len(),
root.display(),
);
let parser = root.join("crates/ridl-syntax/src/parser.rs");
let emitters: Vec<String> = sources
.iter()
.filter(|path| {
let text = std::fs::read_to_string(path)
.unwrap_or_else(|err| panic!("cannot read {}: {err}", path.display()));
let stripped = strip_line_comments(&text);
stripped
.match_indices(r#""expected {"#)
.any(|(at, matched)| is_whole_debug_message(&stripped[at + matched.len()..]))
})
.map(|path| path.display().to_string())
.collect();
assert_eq!(
emitters,
vec![parser.display().to_string()],
"the workspace no longer has exactly one message that is a \
`Debug`-formatted token name and nothing else, so the reachable \
kinds are no longer just `expect`'s arguments in the parser. Either \
route the new emitter through `expect`, or widen the scan below to \
read its argument too",
);
let text = std::fs::read_to_string(&parser)
.unwrap_or_else(|err| panic!("cannot read {}: {err}", parser.display()));
let despaced: String = strip_line_comments(&text)
.chars()
.filter(|c| !c.is_whitespace())
.collect();
let mut expectable = std::collections::BTreeSet::new();
for (at, _) in despaced.match_indices(CALL) {
let rest = &despaced[at + CALL.len()..];
let end = rest
.find(')')
.expect("an `expect` call closes its parenthesis");
expectable.insert(rest[..end].to_string());
}
assert!(
expectable.len() >= 7,
"the scan found only {} expectable kinds in {} — it is not reading \
the call sites",
expectable.len(),
parser.display(),
);
for kind in &expectable {
assert!(
punctuation_glyph(kind).is_some(),
"the parser can emit `expected {kind}`, which renders the raw \
`Debug` name to the user. Add `{kind}` to `punctuation_glyph`.",
);
}
}
#[test]
fn house_style_passes_through_already_styled_messages() {
assert_eq!(house_style_message("expected a name"), "expected a name");
assert_eq!(house_style_message("unclosed `{`"), "unclosed `{`");
assert_eq!(
house_style_message("missing `package` declaration"),
"missing `package` declaration",
);
}
#[test]
fn retired_ridl_codes_are_never_redeclared() {
for entry in RIDL_CATALOG {
let code = entry.code.as_str();
let number: u16 = code
.strip_prefix("RIDL-")
.and_then(|digits| digits.parse().ok())
.unwrap_or_else(|| panic!("`{code}` is not spelled `RIDL-NNN`"));
assert!(
!RETIRED_RIDL_CODES.contains(&number),
"`{code}` is retired and must not be declared again",
);
}
}
#[test]
fn retired_typl_codes_are_never_redeclared() {
for entry in TYPL_CATALOG {
let code = entry.code.as_str();
let number: u16 = code
.strip_prefix("TYPL-")
.and_then(|digits| digits.parse().ok())
.unwrap_or_else(|| panic!("`{code}` is not spelled `TYPL-NNN`"));
assert!(
!RETIRED_TYPL_CODES.contains(&number),
"`{code}` is retired and must not be declared again",
);
}
}
#[test]
fn catalog_entries_are_well_formed_ordered_and_unique() {
let mut seen: Vec<&str> = Vec::new();
for (name, catalog) in ALL_CATALOGS {
let prefix = name
.strip_suffix("_CATALOG")
.unwrap_or_else(|| panic!("`{name}` is not named `<PREFIX>_CATALOG`"));
let mut previous = "";
for entry in *catalog {
let code = entry.code.as_str();
let (written_prefix, number) = code
.split_once('-')
.unwrap_or_else(|| panic!("`{code}` is not spelled `PREFIX-NNN`"));
assert_eq!(
written_prefix, prefix,
"`{code}` is listed in {name}, which holds the `{prefix}-` namespace",
);
assert!(
number.len() == 3 && number.bytes().all(|byte| byte.is_ascii_digit()),
"`{code}` does not carry a three-digit number",
);
assert!(!entry.summary.is_empty(), "`{code}` has an empty summary");
assert!(
previous < code,
"{name} is out of order: `{previous}` is listed before `{code}`",
);
previous = code;
assert!(
!seen.contains(&code),
"`{code}` is declared in more than one catalogue",
);
seen.push(code);
}
}
assert!(
seen.len() >= 90,
"only {} codes reached the catalogues — the guards below are \
checking almost nothing",
seen.len(),
);
}
#[test]
fn allow_by_default_is_only_on_warning_or_info_rows() {
for entry in ALL_CATALOGS.iter().flat_map(|(_, c)| c.iter()) {
if entry.allow_by_default {
assert_ne!(entry.severity, Severity::Error, "{}", entry.code.as_str());
assert!(entry.lint.is_some(), "{}", entry.code.as_str());
}
}
}
#[test]
fn lint_names_are_present_exactly_on_warnings_and_infos_and_unique() {
fn is_lint_name(name: &str) -> bool {
name.starts_with(|c: char| c.is_ascii_lowercase())
&& name.split('-').all(|word| {
!word.is_empty()
&& word
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
})
}
let mut names = std::collections::BTreeSet::new();
let mut pairs = std::collections::BTreeSet::new();
for (_, catalog) in ALL_CATALOGS {
for entry in *catalog {
let code = entry.code.as_str();
match entry.severity {
Severity::Error => assert_eq!(
entry.lint, None,
"`{code}` is an Error and must not have a lint name",
),
Severity::Warning | Severity::Info => {
let name = entry.lint.unwrap_or_else(|| {
panic!("`{code}` is a Warning or Info and has no lint name")
});
assert!(
is_lint_name(name),
"`{code}` has the lint name `{name}`, which does not \
match `^[a-z][a-z0-9]*(-[a-z0-9]+)*$`",
);
assert!(
names.insert(name),
"the lint name `{name}` is used by more than one row",
);
pairs.insert((code, name));
}
}
}
}
let expected: std::collections::BTreeSet<(&str, &str)> = [
("TYPL-007", "unused-import"),
("TYPL-008", "unneeded-import-alias"),
("TYPL-101", "unbounded-integer"),
("TYPL-102", "unbounded-float"),
("TYPL-103", "unbounded-length"),
("TYPL-115", "no-init-value"),
("TYPL-211", "duplicate-reserved"),
("TYPL-222", "inconsistent-unit"),
("TYPL-224", "duplicate-shape"),
("TYPL-401", "broken-doc-link"),
("TYPL-404", "detached-doc-comment"),
("TYPL-405", "deprecated-without-reason"),
("TYPL-406", "missing-docs"),
("TYPL-407", "misplaced-doc-comment"),
("TYPL-408", "unknown-doc-tag"),
("TYPL-409", "malformed-doc-tag"),
("TYPL-410", "doc-comment-style"),
("RIDL-100", "missing-timing"),
("RIDL-108", "degenerate-timing-range"),
("RIDL-112", "missing-response-bound"),
("RIDL-304", "error-typed-parameter"),
("RIDL-305", "ensure-without-result"),
("RIDL-307", "contract-error-name-in-enum"),
("RIDL-308", "named-result-union-in-query"),
("RIDL-404", "query-named-like-mutation"),
("RIDL-405", "shared-error-type"),
("RIDL-406", "redeclared-envelope-metadata"),
("RIDL-407", "ordinal-changed"),
("RIDL-414", "low-cohesion-interface"),
("RSDL-409", "redundant-provider-set"),
("RSDL-804", "unclaimed-backend-key"),
("RSDL-805", "depth-below-bound"),
("RSDL-806", "depth-underivable"),
("MANI-005", "unknown-manifest-key"),
("MANI-010", "unknown-lint"),
]
.into_iter()
.collect();
assert_eq!(
pairs, expected,
"the lint names differ from the expected list in this test"
);
}
#[test]
fn each_constant_name_is_the_code_it_expands_to() {
for (name, code) in CODE_CONSTANT_NAMES {
assert_eq!(
&name.replace('_', "-"),
code,
"`DiagCode::{name}` expands to `{code}`",
);
}
assert!(CODE_CONSTANT_NAMES.len() >= 90, "the entry list went empty");
}
#[test]
fn form_and_mani_severities_follow_their_namespace_rule() {
for entry in FORM_CATALOG {
assert_eq!(
entry.severity,
Severity::Error,
"every FORM code is an error, but {} is not",
entry.code.as_str(),
);
}
for entry in MANI_CATALOG {
let expected = if entry.code == DiagCode::MANI_005 || entry.code == DiagCode::MANI_010 {
Severity::Warning
} else {
Severity::Error
};
assert_eq!(
entry.severity,
expected,
"unexpected severity for {}",
entry.code.as_str(),
);
}
}
#[test]
fn codes_written_as_string_literals_are_all_catalogued() {
let root = workspace_root();
let mut sources = Vec::new();
collect_rust_sources(&root, &mut sources);
let catalogued: std::collections::BTreeSet<&str> = ALL_CATALOGS
.iter()
.flat_map(|(_, catalog)| catalog.iter().map(|entry| entry.code.as_str()))
.collect();
let mut uncatalogued: Vec<String> = Vec::new();
let mut files_holding_codes = 0usize;
let mut codes_outside_this_module: std::collections::BTreeSet<String> =
std::collections::BTreeSet::new();
for path in &sources {
let text = std::fs::read_to_string(path)
.unwrap_or_else(|err| panic!("cannot read {}: {err}", path.display()));
let text = strip_line_comments(&text);
let literals = code_literals(&text);
if !literals.is_empty() {
files_holding_codes += 1;
}
let is_this_module = path.ends_with("ridl-core/src/diag.rs");
for code in literals {
if !catalogued.contains(code) {
uncatalogued.push(format!("{}: {code}", path.display()));
}
if !is_this_module {
codes_outside_this_module.insert(code.to_string());
}
}
}
assert!(
uncatalogued.is_empty(),
"these code strings are written in Rust sources but no catalogue \
lists them, so the error index has nothing to key them on \
and nothing connects them to a `DiagCode`. Declare each one in \
`diag_codes!`:\n{}",
uncatalogued.join("\n"),
);
assert!(
sources.len() >= 60,
"the walk found only {} `.rs` files under {} — it is not reaching \
the workspace",
sources.len(),
root.display(),
);
assert!(
files_holding_codes >= 10,
"only {files_holding_codes} files carried a code literal",
);
assert!(
codes_outside_this_module.len() >= 60,
"only {} distinct codes were seen outside `diag.rs` — the scan is \
checking this module against itself",
codes_outside_this_module.len(),
);
}
#[test]
fn no_diagnostic_constant_is_declared_outside_the_macro() {
let anchor = format!("DiagCode{}", '=');
let in_macro = format!("pubconst$konst:{anchor}DiagCode($code);");
let sentinel = format!("pubconstNONE:{anchor}DiagCode(\"\");");
let accepted = [in_macro.as_str(), sentinel.as_str()];
let root = workspace_root();
let mut sources = Vec::new();
collect_rust_sources(&root, &mut sources);
assert!(
sources.len() >= 60,
"the walk found only {} `.rs` files under {} — it is not reaching \
the workspace",
sources.len(),
root.display(),
);
let mut declarations = 0usize;
for path in &sources {
let text = std::fs::read_to_string(path)
.unwrap_or_else(|err| panic!("cannot read {}: {err}", path.display()));
let stripped = strip_line_comments(&text);
let despaced: String = stripped.chars().filter(|c| !c.is_whitespace()).collect();
for (at, _) in despaced.match_indices(&anchor) {
let recognised = accepted.iter().any(|form| {
let offset = form.find(&anchor).expect("each form holds the anchor");
at >= offset && despaced[at - offset..].starts_with(form)
});
assert!(
recognised,
"{}: `…{}…` declares a `DiagCode` constant outside \
`diag_codes!`, so it carries no catalogue entry. Move it \
into the macro.",
path.display(),
&despaced[at.saturating_sub(40)..(at + 40).min(despaced.len())],
);
declarations += 1;
}
}
assert_eq!(
declarations, 2,
"expected exactly two `DiagCode` constant declarations in the \
workspace — the macro body and the `NONE` sentinel, both in this \
file",
);
}
#[test]
fn the_overview_form_and_mani_tables_mirror_the_catalogues() {
let overview = workspace_root().join("docs/specification/ridl-family-overview.md");
let text = std::fs::read_to_string(&overview)
.unwrap_or_else(|err| panic!("cannot read {}: {err}", overview.display()));
let mut documented: Vec<(String, Severity)> = Vec::new();
for line in text.lines() {
let mut cells = line.split('|').map(str::trim);
if cells.next() != Some("") {
continue;
}
let (Some(code), Some(_rule), Some(severity)) =
(cells.next(), cells.next(), cells.next())
else {
continue;
};
if !(code.starts_with("FORM-") || code.starts_with("MANI-")) {
continue;
}
let severity = match severity {
"error" => Severity::Error,
"warning" => Severity::Warning,
"info" => Severity::Info,
other => panic!("{code} in the overview carries no severity: `{other}`"),
};
documented.push((code.to_string(), severity));
}
let declared: Vec<(String, Severity)> = FORM_CATALOG
.iter()
.chain(MANI_CATALOG)
.map(|entry| (entry.code.as_str().to_string(), entry.severity))
.collect();
let names = |rows: &[(String, Severity)]| -> Vec<String> {
let mut names: Vec<String> = rows.iter().map(|(code, _)| code.clone()).collect();
names.sort();
names
};
assert_eq!(
names(&documented),
names(&declared),
"the family overview §7 tables and the FORM/MANI catalogues list \
different codes — {} documents the two namespaces `diag.rs` owns, \
so a code minted in one belongs in the other",
overview.display(),
);
for (code, severity) in &declared {
let (_, documented_severity) = documented
.iter()
.find(|(name, _)| name == code)
.expect("the code sets are equal");
assert_eq!(
documented_severity, severity,
"{code} is {severity:?} in the catalogue and \
{documented_severity:?} in the overview",
);
}
assert!(
documented.len() >= 25,
"only {} rows were read out of the overview tables — the parse is \
not finding them",
documented.len(),
);
}
fn is_whole_debug_message(rest: &str) -> bool {
let Some(colon) = rest.find(':') else {
return false;
};
let placeholder = &rest[..colon];
!placeholder.is_empty()
&& placeholder
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
&& rest[colon..].starts_with(":?}\"")
}
fn strip_line_comments(text: &str) -> String {
text.lines()
.map(|line| match line.find("//") {
Some(at) => &line[..at],
None => line,
})
.collect::<Vec<_>>()
.join("\n")
}
fn workspace_root() -> std::path::PathBuf {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(std::path::Path::parent)
.expect("`crates/ridl-core` sits two levels below the workspace root")
.to_path_buf();
let manifest = std::fs::read_to_string(root.join("Cargo.toml"))
.unwrap_or_else(|err| panic!("no manifest at {}: {err}", root.display()));
assert!(
manifest.contains("[workspace]"),
"{} is not the workspace root",
root.display(),
);
root
}
fn collect_rust_sources(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
let entries = std::fs::read_dir(dir)
.unwrap_or_else(|err| panic!("cannot read {}: {err}", dir.display()));
for entry in entries {
let entry = entry.unwrap_or_else(|err| panic!("cannot read {}: {err}", dir.display()));
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with('.') || name == "target" {
continue;
}
let path = entry.path();
if path.is_dir() {
collect_rust_sources(&path, out);
} else if path.extension().is_some_and(|extension| extension == "rs") {
out.push(path);
}
}
}
fn code_literals(text: &str) -> Vec<&str> {
let bytes = text.as_bytes();
let mut found = Vec::new();
for dash in 0..bytes.len() {
if bytes[dash] != b'-' || dash + 4 >= bytes.len() || bytes[dash + 4] != b'"' {
continue;
}
if !bytes[dash + 1..dash + 4].iter().all(u8::is_ascii_digit) {
continue;
}
let mut start = dash;
while start > 0 && bytes[start - 1].is_ascii_uppercase() {
start -= 1;
}
if dash - start < 2 || start == 0 || bytes[start - 1] != b'"' {
continue;
}
found.push(&text[start..dash + 4]);
}
found
}
}
#[cfg(test)]
mod json_tests {
use super::*;
#[test]
fn line_col_is_one_based_and_counts_chars() {
let text = "ab\ncé\n";
assert_eq!(
line_col(text, TextSize::from(0)),
LineCol { line: 1, column: 1 }
);
assert_eq!(
line_col(text, TextSize::from(2)),
LineCol { line: 1, column: 3 }
);
assert_eq!(
line_col(text, TextSize::from(3)),
LineCol { line: 2, column: 1 }
);
assert_eq!(
line_col(text, TextSize::from(6)),
LineCol { line: 2, column: 3 }
);
assert_eq!(
line_col(text, TextSize::from(99)),
LineCol { line: 3, column: 1 }
);
}
#[test]
fn to_json_projects_spans_and_fixits_onto_lines_and_columns() {
let mut sources = SourceMap::new();
let file = sources.file_id("a.typl", "package p\ntype X:\n");
let span = Span {
file,
range: TextRange::new(TextSize::from(10), TextSize::from(17)),
};
let fixit_span = Span {
file,
range: TextRange::new(TextSize::from(8), TextSize::from(9)),
};
let diagnostic = Diagnostic {
code: DiagCode::TYPL_009,
severity: Severity::Error,
message: "expected a type".to_string(),
primary: span,
labels: Vec::new(),
fixits: vec![FixIt {
span: fixit_span,
replacement: "type X: integer".to_string(),
label: "give `X` a backing type".to_string(),
}],
};
let json = to_json(&[diagnostic], &sources);
assert_eq!(json.len(), 1);
let first = &json[0];
assert_eq!(first.code, "TYPL-009");
assert_eq!(first.severity, "error");
assert_eq!(first.message, "expected a type");
assert_eq!(first.span.path, "a.typl");
assert_eq!(first.span.start, LineCol { line: 2, column: 1 });
assert_eq!(first.span.end, LineCol { line: 2, column: 8 });
assert_eq!(first.fixes.len(), 1);
assert_eq!(first.fixes[0].label, "give `X` a backing type");
assert_eq!(first.fixes[0].replacement, "type X: integer");
assert_eq!(
first.fixes[0].span.start,
LineCol { line: 1, column: 9 },
"the fix-it's span is its own, not the primary span",
);
assert_eq!(
first.fixes[0].span.end,
LineCol {
line: 1,
column: 10
}
);
}
#[test]
fn to_json_snapshot_pins_the_wire_key_names() {
let mut sources = SourceMap::new();
let file = sources.file_id("a.typl", "package p\ntype X:\n");
let span = Span {
file,
range: TextRange::new(TextSize::from(10), TextSize::from(17)),
};
let fixit_span = Span {
file,
range: TextRange::new(TextSize::from(8), TextSize::from(9)),
};
let diagnostic = Diagnostic {
code: DiagCode::TYPL_009,
severity: Severity::Error,
message: "expected a type".to_string(),
primary: span,
labels: Vec::new(),
fixits: vec![FixIt {
span: fixit_span,
replacement: "type X: integer".to_string(),
label: "give `X` a backing type".to_string(),
}],
};
insta::assert_json_snapshot!(to_json(&[diagnostic], &sources));
}
#[test]
fn to_json_names_the_lint_and_omits_it_for_an_error_code() {
let mut sources = SourceMap::new();
let file = sources.file_id("a.ridl", "package p\n");
let span = Span {
file,
range: TextRange::new(TextSize::from(0), TextSize::from(7)),
};
let diagnostic = |code, severity| Diagnostic {
code,
severity,
message: "m".to_string(),
primary: span,
labels: Vec::new(),
fixits: Vec::new(),
};
let json = to_json(
&[
diagnostic(DiagCode::RIDL_100, Severity::Warning),
diagnostic(DiagCode::TYPL_009, Severity::Error),
],
&sources,
);
assert_eq!(json[0].code, "RIDL-100");
assert_eq!(json[0].lint.as_deref(), Some("missing-timing"));
assert_eq!(json[1].code, "TYPL-009");
assert_eq!(json[1].lint, None);
insta::assert_json_snapshot!("to_json_lint_field", json);
}
#[test]
fn to_json_tolerates_a_span_on_an_unknown_file() {
let sources = SourceMap::new();
let diagnostic = Diagnostic {
code: DiagCode::MANI_101,
severity: Severity::Warning,
message: "detached".to_string(),
primary: Span {
file: FileId::DETACHED,
range: TextRange::new(TextSize::from(0), TextSize::from(0)),
},
labels: Vec::new(),
fixits: Vec::new(),
};
let json = to_json(&[diagnostic], &sources);
assert_eq!(json[0].severity, "warning");
assert_eq!(json[0].span.path, "");
assert_eq!(json[0].span.start, LineCol { line: 1, column: 1 });
}
#[test]
fn to_json_resolves_spans_in_the_second_of_two_files() {
let mut sources = SourceMap::new();
let _first = sources.file_id("a.typl", "package p\n");
let second = sources.file_id("b.typl", "package p\nimport a\ntype Y:\n");
let primary = Span {
file: second,
range: TextRange::new(TextSize::from(19), TextSize::from(26)),
};
let label_span = Span {
file: second,
range: TextRange::new(TextSize::from(10), TextSize::from(18)),
};
let diagnostic = Diagnostic {
code: DiagCode::TYPL_009,
severity: Severity::Error,
message: "expected a type".to_string(),
primary,
labels: vec![Label {
span: label_span,
message: "imported here".to_string(),
}],
fixits: Vec::new(),
};
let json = to_json(&[diagnostic], &sources);
let first = &json[0];
assert_eq!(first.span.path, "b.typl");
assert_eq!(first.span.start, LineCol { line: 3, column: 1 });
assert_eq!(first.span.end, LineCol { line: 3, column: 8 });
assert_eq!(first.labels.len(), 1);
assert_eq!(first.labels[0].message, "imported here");
assert_eq!(first.labels[0].span.path, "b.typl");
assert_eq!(first.labels[0].span.start, LineCol { line: 2, column: 1 });
assert_eq!(first.labels[0].span.end, LineCol { line: 2, column: 9 });
}
#[test]
fn to_json_reports_info_severity() {
let sources = SourceMap::new();
let diagnostic = Diagnostic {
code: DiagCode::TYPL_115,
severity: Severity::Info,
message: "type has no derivable init value".to_string(),
primary: Span {
file: FileId::DETACHED,
range: TextRange::new(TextSize::from(0), TextSize::from(0)),
},
labels: Vec::new(),
fixits: Vec::new(),
};
let json = to_json(&[diagnostic], &sources);
assert_eq!(json[0].severity, "info");
}
#[test]
fn to_json_keeps_the_code_field_present_when_empty() {
let sources = SourceMap::new();
let diagnostic = Diagnostic {
code: DiagCode::NONE,
severity: Severity::Error,
message: "expected a type, but `FROB` names a constant".to_string(),
primary: Span {
file: FileId::DETACHED,
range: TextRange::new(TextSize::from(0), TextSize::from(0)),
},
labels: Vec::new(),
fixits: Vec::new(),
};
insta::assert_json_snapshot!(to_json(&[diagnostic], &sources));
}
}