use std::fmt::{self, Display, Formatter};
use crate::parser::{self, ParseError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceSpan
{
pub start: usize,
pub end: usize
}
impl Display for SourceSpan
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result
{
write!(f, "{}..{}", self.start, self.end)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiagnosticKind
{
UnclosedDelimiter
{
opener: char,
expected_closer: char
},
UnopenedDelimiter
{
closer: char
},
MissingRightOperand
{
operator: char
},
MissingLeftOperand
{
operator: char
},
BareIdentifier,
MissingDiceFaces,
IncompleteDropClause,
IncompleteParameterDefinition,
TrailingInput,
EmptyExpression,
UnexpectedToken,
UnexpectedEof
}
impl Display for DiagnosticKind
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result
{
match self
{
Self::UnclosedDelimiter { opener, .. } =>
{
write!(f, "unclosed `{}`", opener)
},
Self::UnopenedDelimiter { closer } =>
{
write!(f, "unexpected `{}`", closer)
},
Self::MissingRightOperand { operator } =>
{
write!(f, "missing right operand of `{}`", operator)
},
Self::MissingLeftOperand { operator } =>
{
write!(f, "missing left operand of `{}`", operator)
},
Self::BareIdentifier => write!(f, "bare identifier"),
Self::MissingDiceFaces => write!(f, "missing dice faces"),
Self::IncompleteDropClause =>
{
write!(f, "incomplete drop clause")
},
Self::IncompleteParameterDefinition =>
{
write!(f, "incomplete parameter definition")
},
Self::TrailingInput => write!(f, "trailing input"),
Self::EmptyExpression => write!(f, "empty expression"),
Self::UnexpectedToken => write!(f, "unexpected token"),
Self::UnexpectedEof => write!(f, "unexpected end of input")
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic
{
pub kind: DiagnosticKind,
pub span: SourceSpan,
pub message: String,
pub suggestions: Vec<Suggestion>
}
impl Display for Diagnostic
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result
{
write!(f, "{} ({}): {}", self.kind, self.span, self.message)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Suggestion
{
pub description: String,
pub corrected_source: String,
pub placeholders: Vec<Placeholder>
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Placeholder
{
pub span: SourceSpan,
pub description: &'static str,
pub valid_kinds: &'static [&'static str]
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagnoseResult
{
pub diagnostics: Vec<Diagnostic>,
pub corrected_source: Option<String>
}
const OPERAND_KINDS: &[&str] =
&["integer", "{variable}", "(expression)", "dice expression"];
const FACE_COUNT_KINDS: &[&str] = &["integer", "{variable}", "(expression)"];
const DROP_DIRECTION_KINDS: &[&str] = &["lowest", "highest"];
fn analyze_error(
source: &str,
error: &ParseError<'_>,
offset_map: &OffsetMap
) -> Diagnostic
{
let rightmost = &error.errors[0];
let pos = rightmost.0.location_offset();
let at_eof =
pos >= source.len() || source[pos..].chars().all(char::is_whitespace);
let expectations = error
.errors
.iter()
.take_while(|(span, _)| span.location_offset() == pos)
.flat_map(|(_, kind)| kind.expectations())
.collect::<Vec<_>>();
let expects_expression = expectations.iter().any(|e| {
let s = e.as_ref();
s == "integer"
|| s == "dice expression"
|| s == "`(`"
|| s == "`{`"
|| s == "`[`"
|| s == "`-`"
});
let expects_delimiter =
expectations.iter().find_map(|e| match e.as_ref()
{
"`)`" => Some(('(', ')')),
"`]`" => Some(('[', ']')),
"`}`" => Some(('{', '}')),
_ => None
});
if at_eof && let Some((opener, closer)) = expects_delimiter
{
let opener_pos = find_unmatched_opener(source, opener, closer, pos);
return make_unclosed_delimiter(
source, opener, closer, opener_pos, pos, offset_map
);
}
if expects_expression
&& !at_eof
&& let Some(diag) =
detect_bare_identifier_at_pos(source, pos, offset_map)
{
return diag;
}
let expects_identifier =
expectations.iter().any(|e| e.as_ref() == "identifier");
if (expects_expression || expects_identifier)
&& let Some((prev, prev_pos)) = find_preceding_char(source, pos)
{
if prev == 'd' || prev == 'D'
{
return make_missing_dice_faces(source, prev_pos, pos, offset_map);
}
if "+-*×/÷%^".contains(prev)
{
return make_missing_right_operand(
source, prev, prev_pos, pos, offset_map
);
}
if prev == '(' || prev == '[' || prev == '{'
{
return make_incomplete_delimited(
source, prev, prev_pos, pos, offset_map
);
}
if prev == ':'
&& let Some(bracket_pos) = source[..prev_pos].rfind('[')
{
let orig_bracket = offset_map.to_original(bracket_pos);
let mut corrected = source[..pos].to_string();
let p_start = corrected.len();
corrected.push('0');
let p_end = corrected.len();
corrected.push(']');
corrected.push_str(&source[pos..]);
return Diagnostic {
kind: DiagnosticKind::UnclosedDelimiter {
opener: '[',
expected_closer: ']'
},
span: SourceSpan {
start: orig_bracket,
end: orig_bracket + 1
},
message: "expected `]` to close `[`".into(),
suggestions: vec![Suggestion {
description: "insert range end \
and `]`"
.into(),
corrected_source: corrected,
placeholders: vec![Placeholder {
span: SourceSpan {
start: p_start,
end: p_end
},
description: "range end",
valid_kinds: OPERAND_KINDS
}]
}]
};
}
}
if expectations
.iter()
.any(|e| e.as_ref() == "`lowest`" || e.as_ref() == "`highest`")
{
let drop_pos =
source[..pos].rfind("drop").unwrap_or(pos.saturating_sub(5));
let orig_drop = offset_map.to_original(drop_pos);
let orig_end = offset_map.to_original((drop_pos + 4).min(source.len()));
return make_incomplete_drop(
source, orig_drop, orig_end, pos, offset_map
);
}
if !at_eof
{
let ch = source[pos..].chars().next().unwrap_or('\0');
if "+-*×/÷%^".contains(ch) && pos == 0
{
return make_missing_left_operand(source, ch, offset_map);
}
}
if source.trim().is_empty()
{
return make_empty_expression(source);
}
if let Some(diag) = detect_bare_identifier(source, pos, offset_map)
{
return diag;
}
if expectations
.iter()
.any(|e| e.as_ref() == "`,`" || e.as_ref() == "`:`")
&& let Some(diag) = detect_incomplete_parameter(source, pos, offset_map)
{
return diag;
}
if expectations.iter().any(|e| e.as_ref() == "end of input") && !at_eof
{
let orig_pos = offset_map.to_original(pos);
let orig_end = offset_map.to_original(source.len());
return Diagnostic {
kind: DiagnosticKind::TrailingInput,
span: SourceSpan {
start: orig_pos,
end: orig_end
},
message: format!(
"unexpected `{}` after expression",
&source[pos..].split_whitespace().next().unwrap_or("")
),
suggestions: vec![Suggestion {
description: "remove trailing input".into(),
corrected_source: source[..pos].trim_end().to_string(),
placeholders: vec![]
}]
};
}
let orig_pos = offset_map.to_original(pos);
if at_eof
{
Diagnostic {
kind: DiagnosticKind::UnexpectedEof,
span: SourceSpan {
start: orig_pos,
end: orig_pos
},
message: "unexpected end of input".into(),
suggestions: vec![]
}
}
else
{
let token_end = source[pos..]
.find(|c: char| c.is_whitespace())
.map_or(source.len(), |i| pos + i);
let orig_end = offset_map.to_original(token_end);
Diagnostic {
kind: DiagnosticKind::UnexpectedToken,
span: SourceSpan {
start: orig_pos,
end: orig_end
},
message: format!("unexpected `{}`", &source[pos..token_end]),
suggestions: vec![]
}
}
}
fn find_preceding_char(source: &str, pos: usize) -> Option<(char, usize)>
{
let before = &source[..pos];
for (i, c) in before.char_indices().rev()
{
if !c.is_whitespace()
{
return Some((c, i));
}
}
None
}
fn find_unmatched_opener(
source: &str,
opener: char,
closer: char,
pos: usize
) -> Option<usize>
{
let mut depth = 0i32;
let mut last_opener = None;
for (i, c) in source[..pos].char_indices()
{
if c == opener
{
depth += 1;
last_opener = Some(i);
}
else if c == closer
{
depth -= 1;
}
}
if depth > 0 { last_opener } else { None }
}
fn detect_bare_identifier_at_pos(
source: &str,
pos: usize,
offset_map: &OffsetMap
) -> Option<Diagnostic>
{
let remaining = &source[pos..];
let first = remaining.chars().next()?;
if !first.is_alphabetic() && first != '_'
{
return None;
}
let ident_end = remaining
.find(|c: char| {
!(c.is_alphanumeric()
|| c == '_' || c == '-'
|| c == '.' || (c.is_whitespace() && !matches!(c, '\n' | '\r')))
})
.unwrap_or(remaining.len());
let name = remaining[..ident_end].trim_end();
if name.is_empty()
{
return None;
}
let orig_start = offset_map.to_original(pos);
let orig_end = offset_map.to_original(pos + name.len());
let after = &source[pos + ident_end..];
let mut suggestions = Vec::new();
let split_pos = name.char_indices().find(|&(i, c)| {
(c == 'd' || c == 'D')
&& i > 0 && name[..i]
.starts_with(|c: char| c.is_alphabetic() || c == '_')
});
if let Some((d_offset, _)) = split_pos
{
let prefix = &name[..d_offset];
let suffix = &name[d_offset..];
let mut split_fix = source[..pos].to_string();
split_fix.push('{');
split_fix.push_str(prefix);
split_fix.push('}');
split_fix.push_str(suffix);
split_fix.push_str(after);
suggestions.push(Suggestion {
description: format!("wrap `{}` in braces", prefix),
corrected_source: split_fix,
placeholders: vec![]
});
}
let mut whole_fix = source[..pos].to_string();
whole_fix.push('{');
whole_fix.push_str(name);
whole_fix.push('}');
whole_fix.push_str(after);
suggestions.push(Suggestion {
description: format!("use `{}` as a variable name", name),
corrected_source: whole_fix,
placeholders: vec![]
});
let bare_name = split_pos.map(|(i, _)| &name[..i]).unwrap_or(name);
Some(Diagnostic {
kind: DiagnosticKind::BareIdentifier,
span: SourceSpan {
start: orig_start,
end: orig_end
},
message: format!(
"bare identifier `{}` is not valid here; \
variables must be wrapped in `{{}}`",
bare_name
),
suggestions
})
}
fn detect_incomplete_parameter(
source: &str,
pos: usize,
offset_map: &OffsetMap
) -> Option<Diagnostic>
{
let trimmed = source.trim();
if trimmed.is_empty()
{
return None;
}
let params_text = source[..pos].trim();
if params_text.is_empty()
{
return None;
}
let params: Vec<&str> = params_text
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
if params.is_empty()
|| !params
.iter()
.all(|p| p.starts_with(|c: char| c.is_alphabetic() || c == '_'))
{
return None;
}
let orig_start = offset_map.to_original(0);
let orig_end = offset_map.to_original(pos);
let mut suggestions = Vec::new();
let trailing = source[pos..].trim_start();
let var_suggestion = if params.len() == 1
{
let name = params[0];
let mut var_fix = format!("{{{}}}", name);
if !trailing.is_empty()
{
var_fix.push(' ');
var_fix.push_str(trailing);
}
Some(Suggestion {
description: format!("use `{}` as a variable reference", name),
corrected_source: var_fix,
placeholders: vec![]
})
}
else
{
None
};
let param_suggestion = {
let mut fix = source[..pos].to_string();
fix.push_str(": ");
let trailing_works = if !trailing.is_empty()
{
let mut candidate = fix.clone();
candidate.push_str(trailing);
parser::parse(&candidate).is_ok()
}
else
{
false
};
if trailing_works
{
fix.push_str(trailing);
Suggestion {
description: "complete parameter definition".into(),
corrected_source: fix,
placeholders: vec![]
}
}
else
{
let placeholder_start = fix.len();
fix.push('0');
let placeholder_end = fix.len();
Suggestion {
description: "complete parameter definition".into(),
corrected_source: fix,
placeholders: vec![Placeholder {
span: SourceSpan {
start: placeholder_start,
end: placeholder_end
},
description: "expression",
valid_kinds: OPERAND_KINDS
}]
}
}
};
if !trailing.is_empty()
{
if let Some(vs) = var_suggestion
{
suggestions.push(vs);
}
suggestions.push(param_suggestion);
}
else
{
suggestions.push(param_suggestion);
if let Some(vs) = var_suggestion
{
suggestions.push(vs);
}
}
Some(Diagnostic {
kind: DiagnosticKind::IncompleteParameterDefinition,
span: SourceSpan {
start: orig_start,
end: orig_end
},
message: format!(
"expected `:` and expression body after parameter{}",
if params.len() > 1 { "s" } else { "" }
),
suggestions
})
}
fn detect_bare_identifier(
source: &str,
error_pos: usize,
offset_map: &OffsetMap
) -> Option<Diagnostic>
{
let before = &source[..error_pos];
let ident_start = before
.char_indices()
.rev()
.take_while(|(_, c)| {
c.is_alphanumeric() || *c == '_' || *c == '-' || *c == '.'
})
.last()
.map(|(i, _)| i)?;
let ident = &source[ident_start..error_pos];
for (i, c) in ident.char_indices()
{
if (c == 'd' || c == 'D') && i > 0
{
let after_d = &ident[i + 1..];
if after_d.is_empty()
|| after_d.starts_with(|c: char| c.is_ascii_digit())
|| after_d.starts_with('[')
|| after_d.starts_with('(')
|| after_d.starts_with('{')
{
let name = &ident[..i];
if name.starts_with(|c: char| c.is_alphabetic() || c == '_')
{
let orig_start = offset_map.to_original(ident_start);
let orig_end = offset_map.to_original(ident_start + i);
let after = &source[ident_start + i..];
let mut split_fix = source[..ident_start].to_string();
split_fix.push('{');
split_fix.push_str(name);
split_fix.push('}');
split_fix.push_str(after);
let mut whole_fix = source[..ident_start].to_string();
whole_fix.push('{');
whole_fix.push_str(ident);
whole_fix.push('}');
whole_fix.push_str(&source[error_pos..]);
return Some(Diagnostic {
kind: DiagnosticKind::BareIdentifier,
span: SourceSpan {
start: orig_start,
end: orig_end
},
message: format!(
"bare identifier `{}` is not valid here; \
variables must be wrapped in `{{}}`",
name
),
suggestions: vec![
Suggestion {
description: format!(
"wrap `{}` in braces",
name
),
corrected_source: split_fix,
placeholders: vec![]
},
Suggestion {
description: format!(
"use `{}` as a variable \
name",
ident
),
corrected_source: whole_fix,
placeholders: vec![]
},
]
});
}
}
}
}
None
}
fn make_unclosed_delimiter(
source: &str,
opener: char,
closer: char,
opener_pos: Option<usize>,
error_pos: usize,
offset_map: &OffsetMap
) -> Diagnostic
{
let actual_opener = opener_pos.unwrap_or(0);
let orig_opener = offset_map.to_original(actual_opener);
let content_between = source[actual_opener + opener.len_utf8()..error_pos]
.trim()
.is_empty();
let mut corrected = source[..error_pos].to_string();
let mut placeholders = Vec::new();
if content_between
{
let placeholder_start = corrected.len();
corrected.push('0');
let placeholder_end = corrected.len();
placeholders.push(Placeholder {
span: SourceSpan {
start: placeholder_start,
end: placeholder_end
},
description: "expression",
valid_kinds: OPERAND_KINDS
});
}
corrected.push(closer);
corrected.push_str(&source[error_pos..]);
Diagnostic {
kind: DiagnosticKind::UnclosedDelimiter {
opener,
expected_closer: closer
},
span: SourceSpan {
start: orig_opener,
end: orig_opener + opener.len_utf8()
},
message: format!("expected `{}` to close `{}`", closer, opener),
suggestions: vec![Suggestion {
description: format!("insert `{}`", closer),
corrected_source: corrected,
placeholders
}]
}
}
fn make_missing_right_operand(
source: &str,
operator: char,
op_pos: usize,
error_pos: usize,
offset_map: &OffsetMap
) -> Diagnostic
{
let orig_error_pos = offset_map.to_original(error_pos);
let orig_op_pos = offset_map.to_original(op_pos);
let mut insert_fix = source[..error_pos].to_string();
if !insert_fix.is_empty() && !insert_fix.ends_with(' ')
{
insert_fix.push(' ');
}
let placeholder_start = insert_fix.len();
insert_fix.push('0');
let placeholder_end = insert_fix.len();
if !source[error_pos..].is_empty() && !source[error_pos..].starts_with(' ')
{
insert_fix.push(' ');
}
insert_fix.push_str(&source[error_pos..]);
let before_op = source[..op_pos].trim_end();
let mut drop_fix = before_op.to_string();
if !source[error_pos..].is_empty()
{
if !drop_fix.is_empty()
{
drop_fix.push(' ');
}
drop_fix.push_str(source[error_pos..].trim_start());
}
Diagnostic {
kind: DiagnosticKind::MissingRightOperand { operator },
span: SourceSpan {
start: orig_op_pos,
end: orig_error_pos
},
message: format!("expected operand after `{}`", operator),
suggestions: vec![
Suggestion {
description: format!("insert operand after `{}`", operator),
corrected_source: insert_fix,
placeholders: vec![Placeholder {
span: SourceSpan {
start: placeholder_start,
end: placeholder_end
},
description: "operand",
valid_kinds: OPERAND_KINDS
}]
},
Suggestion {
description: format!("remove `{}`", operator),
corrected_source: drop_fix,
placeholders: vec![]
},
]
}
}
fn make_missing_left_operand(
source: &str,
operator: char,
offset_map: &OffsetMap
) -> Diagnostic
{
let orig_pos = offset_map.to_original(0);
let mut insert_fix = String::from("0 ");
insert_fix.push_str(source);
let after_op = source[operator.len_utf8()..].trim_start();
let drop_fix = after_op.to_string();
Diagnostic {
kind: DiagnosticKind::MissingLeftOperand { operator },
span: SourceSpan {
start: orig_pos,
end: orig_pos + operator.len_utf8()
},
message: format!("expected operand before `{}`", operator),
suggestions: vec![
Suggestion {
description: format!("insert operand before `{}`", operator),
corrected_source: insert_fix,
placeholders: vec![Placeholder {
span: SourceSpan { start: 0, end: 1 },
description: "operand",
valid_kinds: OPERAND_KINDS
}]
},
Suggestion {
description: format!("remove `{}`", operator),
corrected_source: drop_fix,
placeholders: vec![]
},
]
}
}
fn make_incomplete_delimited(
source: &str,
opener: char,
opener_pos: usize,
error_pos: usize,
offset_map: &OffsetMap
) -> Diagnostic
{
let orig_opener = offset_map.to_original(opener_pos);
let closer = match opener
{
'(' => ')',
'[' => ']',
'{' => '}',
_ => ')'
};
let mut corrected = source[..error_pos].to_string();
let mut placeholders = Vec::new();
let is_custom_faces = opener == '['
&& opener_pos > 0
&& source[..opener_pos]
.chars()
.last()
.is_some_and(|c| c == 'd' || c == 'D');
match opener
{
'[' if is_custom_faces =>
{
let p_start = corrected.len();
corrected.push('0');
let p_end = corrected.len();
placeholders.push(Placeholder {
span: SourceSpan {
start: p_start,
end: p_end
},
description: "face value",
valid_kinds: &["integer"]
});
},
'[' =>
{
let p1_start = corrected.len();
corrected.push('0');
let p1_end = corrected.len();
corrected.push(':');
let p2_start = corrected.len();
corrected.push('0');
let p2_end = corrected.len();
placeholders.push(Placeholder {
span: SourceSpan {
start: p1_start,
end: p1_end
},
description: "range start",
valid_kinds: OPERAND_KINDS
});
placeholders.push(Placeholder {
span: SourceSpan {
start: p2_start,
end: p2_end
},
description: "range end",
valid_kinds: OPERAND_KINDS
});
},
'{' =>
{
let p_start = corrected.len();
corrected.push('x');
let p_end = corrected.len();
placeholders.push(Placeholder {
span: SourceSpan {
start: p_start,
end: p_end
},
description: "identifier",
valid_kinds: &["identifier"]
});
},
_ =>
{
let p_start = corrected.len();
corrected.push('0');
let p_end = corrected.len();
placeholders.push(Placeholder {
span: SourceSpan {
start: p_start,
end: p_end
},
description: "expression",
valid_kinds: OPERAND_KINDS
});
}
}
corrected.push(closer);
corrected.push_str(&source[error_pos..]);
Diagnostic {
kind: DiagnosticKind::UnclosedDelimiter {
opener,
expected_closer: closer
},
span: SourceSpan {
start: orig_opener,
end: orig_opener + opener.len_utf8()
},
message: format!("expected `{}` to close `{}`", closer, opener),
suggestions: vec![Suggestion {
description: format!("insert expression and `{}`", closer),
corrected_source: corrected,
placeholders
}]
}
}
fn make_missing_dice_faces(
source: &str,
d_pos: usize,
_error_pos: usize,
offset_map: &OffsetMap
) -> Diagnostic
{
let orig_d_pos = offset_map.to_original(d_pos);
let insert_pos = d_pos + 1;
let mut corrected = source[..insert_pos].to_string();
let placeholder_start = corrected.len();
corrected.push('6');
let placeholder_end = corrected.len();
corrected.push_str(&source[insert_pos..]);
Diagnostic {
kind: DiagnosticKind::MissingDiceFaces,
span: SourceSpan {
start: orig_d_pos,
end: orig_d_pos + 1
},
message: format!(
"expected face count after `{}`",
&source[d_pos..d_pos + 1]
),
suggestions: vec![Suggestion {
description: "insert face count".into(),
corrected_source: corrected,
placeholders: vec![Placeholder {
span: SourceSpan {
start: placeholder_start,
end: placeholder_end
},
description: "face count",
valid_kinds: FACE_COUNT_KINDS
}]
}]
}
}
fn make_incomplete_drop(
source: &str,
orig_drop_start: usize,
orig_drop_end: usize,
error_pos: usize,
offset_map: &OffsetMap
) -> Diagnostic
{
let _ = offset_map;
let insert_pos = source[..error_pos]
.rfind("drop")
.map_or(error_pos, |p| p + 4);
let mut corrected = source[..insert_pos].to_string();
corrected.push(' ');
let placeholder_start = corrected.len();
corrected.push_str("lowest");
let placeholder_end = corrected.len();
corrected.push_str(&source[insert_pos..]);
Diagnostic {
kind: DiagnosticKind::IncompleteDropClause,
span: SourceSpan {
start: orig_drop_start,
end: orig_drop_end
},
message: "expected `lowest` or `highest` after `drop`".into(),
suggestions: vec![Suggestion {
description: "insert drop direction".into(),
corrected_source: corrected,
placeholders: vec![Placeholder {
span: SourceSpan {
start: placeholder_start,
end: placeholder_end
},
description: "direction",
valid_kinds: DROP_DIRECTION_KINDS
}]
}]
}
}
fn make_empty_expression(source: &str) -> Diagnostic
{
let _ = source;
Diagnostic {
kind: DiagnosticKind::EmptyExpression,
span: SourceSpan { start: 0, end: 0 },
message: "expected expression".into(),
suggestions: vec![Suggestion {
description: "insert expression".into(),
corrected_source: "0".into(),
placeholders: vec![Placeholder {
span: SourceSpan { start: 0, end: 1 },
description: "expression",
valid_kinds: OPERAND_KINDS
}]
}]
}
}
#[derive(Debug, Clone)]
struct OffsetMap
{
adjustments: Vec<(usize, isize)>
}
impl OffsetMap
{
fn new() -> Self
{
Self {
adjustments: Vec::new()
}
}
fn record(&mut self, modified_pos: usize, delta: isize)
{
self.adjustments.push((modified_pos, delta));
}
fn to_original(&self, modified_pos: usize) -> usize
{
let mut pos = modified_pos as isize;
for &(adj_pos, delta) in self.adjustments.iter().rev()
{
if modified_pos >= adj_pos
{
pos -= delta;
}
}
pos.max(0) as usize
}
}
pub fn diagnose(source: &str) -> DiagnoseResult
{
if parser::parse(source).is_ok()
{
return DiagnoseResult {
diagnostics: vec![],
corrected_source: Some(source.to_string())
};
}
let mut current_source = source.to_string();
let mut diagnostics = Vec::new();
let mut offset_map = OffsetMap::new();
let max_iterations = source.len() + 16;
for _ in 0..max_iterations
{
match parser::parse(¤t_source)
{
Ok(_) =>
{
return DiagnoseResult {
diagnostics,
corrected_source: Some(current_source)
};
},
Err(error) =>
{
let diag = analyze_error(¤t_source, &error, &offset_map);
if let Some(suggestion) = diag.suggestions.first()
{
let old_len = current_source.len() as isize;
let new_source = suggestion.corrected_source.clone();
let new_len = new_source.len() as isize;
let delta = new_len - old_len;
let error_pos = error.errors[0].0.location_offset();
offset_map.record(error_pos, delta);
current_source = new_source;
diagnostics.push(diag);
}
else
{
diagnostics.push(diag);
return DiagnoseResult {
diagnostics,
corrected_source: None
};
}
}
}
}
DiagnoseResult {
diagnostics,
corrected_source: None
}
}
#[cfg(test)]
mod tests
{
use super::OffsetMap;
#[test]
fn test_offset_map_identity()
{
let map = OffsetMap::new();
assert_eq!(map.to_original(0), 0);
assert_eq!(map.to_original(5), 5);
assert_eq!(map.to_original(100), 100);
}
#[test]
fn test_offset_map_single_insertion()
{
let mut map = OffsetMap::new();
map.record(3, 2); assert_eq!(map.to_original(0), 0);
assert_eq!(map.to_original(2), 2);
assert_eq!(map.to_original(3), 1);
assert_eq!(map.to_original(5), 3);
assert_eq!(map.to_original(8), 6);
}
#[test]
fn test_offset_map_single_deletion()
{
let mut map = OffsetMap::new();
map.record(3, -2); assert_eq!(map.to_original(0), 0);
assert_eq!(map.to_original(2), 2);
assert_eq!(map.to_original(3), 5);
assert_eq!(map.to_original(5), 7);
}
#[test]
fn test_offset_map_multiple_insertions()
{
let mut map = OffsetMap::new();
map.record(4, 2); map.record(12, 2); assert_eq!(map.to_original(0), 0);
assert_eq!(map.to_original(3), 3);
assert_eq!(map.to_original(6), 4);
assert_eq!(map.to_original(13), 9);
}
#[test]
fn test_offset_map_position_before_adjustment()
{
let mut map = OffsetMap::new();
map.record(10, 5); for i in 0..10
{
assert_eq!(map.to_original(i), i);
}
}
#[test]
fn test_offset_map_clamp_to_zero()
{
let mut map = OffsetMap::new();
map.record(0, 10); assert_eq!(map.to_original(0), 0);
assert_eq!(map.to_original(5), 0);
assert_eq!(map.to_original(10), 0);
assert_eq!(map.to_original(11), 1);
}
}