use std::{cell::Cell, fmt, rc::Rc};
use tilewright_jsonc::ErrorCode;
pub(crate) trait CodeName: Copy {
fn name(self) -> &'static str;
}
impl CodeName for Code {
fn name(self) -> &'static str {
self.as_str()
}
}
impl CodeName for ErrorCode {
fn name(self) -> &'static str {
self.as_str()
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Code {
Io,
Encoding,
Budget,
Symlink,
UnexpectedFile,
MissingFile,
Path,
NameMismatch,
Format,
UnknownKey,
MissingKey,
Type,
Range,
Identifier,
Duplicate,
Reference,
Cycle,
Grid,
StairwellStep,
StairwellOverlap,
}
impl Code {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Io => "source.io",
Self::Encoding => "source.encoding",
Self::Budget => "source.budget",
Self::Symlink => "source.symlink",
Self::UnexpectedFile => "source.unexpected_file",
Self::MissingFile => "source.missing_file",
Self::Path => "source.path",
Self::NameMismatch => "source.name_mismatch",
Self::Format => "shape.format",
Self::UnknownKey => "shape.unknown_key",
Self::MissingKey => "shape.missing_key",
Self::Type => "shape.type",
Self::Range => "shape.range",
Self::Identifier => "shape.identifier",
Self::Duplicate => "ref.duplicate",
Self::Reference => "ref.unresolved",
Self::Cycle => "ref.cycle",
Self::Grid => "grid.invalid",
Self::StairwellStep => "stairwell.step",
Self::StairwellOverlap => "stairwell.overlap",
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Diagnostic {
pub path: String,
pub start: usize,
pub end: usize,
pub line: usize,
pub column: usize,
pub code: &'static str,
pub message: String,
}
impl Diagnostic {
pub(crate) fn file(path: &str, code: impl CodeName, message: impl Into<String>) -> Self {
Self {
path: path.to_owned(),
start: 0,
end: 0,
line: 0,
column: 0,
code: code.name(),
message: message.into(),
}
}
pub(crate) fn at(
path: &str,
positions: &Positions<'_>,
start: usize,
end: usize,
code: impl CodeName,
message: impl Into<String>,
) -> Self {
let (line, column) = positions.locate(start);
Self {
path: path.to_owned(),
start,
end,
line,
column,
code: code.name(),
message: message.into(),
}
}
}
const STRIDE: usize = 256;
const fn is_continuation(byte: u8) -> bool {
byte & 0xC0 == 0x80
}
pub(crate) struct Positions<'s> {
source: &'s str,
lines: Vec<usize>,
characters: Vec<usize>,
}
impl<'s> Positions<'s> {
pub(crate) fn new(source: &'s str) -> Self {
let mut lines = vec![0];
let mut characters = Vec::with_capacity(source.len() / STRIDE + 1);
let mut count = 0;
for (index, byte) in source.bytes().enumerate() {
if index.is_multiple_of(STRIDE) {
characters.push(count);
}
if byte == b'\n' {
lines.push(index + 1);
}
if !is_continuation(byte) {
count += 1;
}
}
if source.len().is_multiple_of(STRIDE) {
characters.push(count);
}
Self {
source,
lines,
characters,
}
}
fn characters_before(&self, offset: usize) -> usize {
let block = offset / STRIDE;
self.characters[block]
+ self.source.as_bytes()[block * STRIDE..offset]
.iter()
.filter(|byte| !is_continuation(**byte))
.count()
}
pub(crate) fn locate(&self, offset: usize) -> (usize, usize) {
let mut boundary = offset.min(self.source.len());
while !self.source.is_char_boundary(boundary) {
boundary -= 1;
}
let line = self.lines.partition_point(|start| *start <= boundary);
let start = self.lines[line - 1];
(
line,
self.characters_before(boundary) - self.characters_before(start) + 1,
)
}
}
pub(crate) struct Diagnostics {
items: Vec<Diagnostic>,
maximum: usize,
dropped: bool,
skipped: bool,
total: Rc<Cell<usize>>,
}
impl Diagnostics {
pub(crate) fn new(maximum: usize) -> Self {
Self::sharing(maximum, Rc::new(Cell::new(0)))
}
pub(crate) const fn sharing(maximum: usize, total: Rc<Cell<usize>>) -> Self {
Self {
items: Vec::new(),
maximum,
dropped: false,
skipped: false,
total,
}
}
pub(crate) fn total(&self) -> Rc<Cell<usize>> {
Rc::clone(&self.total)
}
pub(crate) fn is_full(&self) -> bool {
self.total.get() >= self.maximum
}
pub(crate) fn push(&mut self, diagnostic: Diagnostic) {
if self.is_full() {
self.dropped = true;
} else {
self.items.push(diagnostic);
self.total.set(self.total.get() + 1);
}
}
pub(crate) const fn is_empty(&self) -> bool {
self.items.is_empty() && !self.dropped && !self.skipped
}
pub(crate) const fn skip(&mut self) {
self.skipped = true;
}
pub(crate) fn append(&mut self, other: Self) {
self.items.extend(other.items);
self.dropped |= other.dropped;
self.skipped |= other.skipped;
}
pub(crate) fn finish(self) -> Vec<Diagnostic> {
let mut items = self.items;
if self.dropped {
items.push(Diagnostic::file(
".",
Code::Budget,
format!(
"more than {} problems; the rest are not reported",
self.maximum
),
));
} else if self.skipped {
items.push(Diagnostic::file(
".",
Code::Budget,
format!(
"{} problems, as many as are reported; reading stopped there, so there may be more",
self.maximum
),
));
}
items.sort();
items.dedup();
items
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.line == 0 {
write!(formatter, "{}: {}: {}", self.path, self.code, self.message)
} else {
write!(
formatter,
"{}:{}:{}: {}: {}",
self.path, self.line, self.column, self.code, self.message
)
}
}
}