Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ pub struct Diagnostic {
pub message: String,
/// Primary source span.
pub span: Span,
/// Ordered source values or constructs valid at the primary span.
pub expected: Vec<String>,
/// Optional corrective guidance.
pub help: Option<String>,
/// Other declarations or references involved in the problem.
Expand All @@ -107,6 +109,7 @@ impl Diagnostic {
severity: Severity::Error,
message: message.into(),
span,
expected: Vec::new(),
help: None,
related: Vec::new(),
}
Expand All @@ -118,6 +121,7 @@ impl Diagnostic {
severity: Severity::Warning,
message: message.into(),
span,
expected: Vec::new(),
help: None,
related: Vec::new(),
}
Expand All @@ -128,6 +132,15 @@ impl Diagnostic {
self
}

pub(crate) fn with_expected<I, S>(mut self, expected: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.expected = expected.into_iter().map(Into::into).collect();
self
}

pub(crate) fn with_related(mut self, message: impl Into<String>, span: Span) -> Self {
self.related.push(RelatedInformation {
message: message.into(),
Expand All @@ -136,3 +149,31 @@ impl Diagnostic {
self
}
}

#[cfg(test)]
mod tests {
use super::{Diagnostic, SourcePosition, Span};

#[test]
fn diagnostics_default_to_no_expected_values() {
let diagnostic = Diagnostic::error(
"STK2002",
"Unexpected value.",
Span::point(SourcePosition::start()),
);

assert!(diagnostic.expected.is_empty());
}

#[test]
fn diagnostics_preserve_expected_value_order() {
let diagnostic = Diagnostic::error(
"STK2002",
"Unknown direction.",
Span::point(SourcePosition::start()),
)
.with_expected(["right", "down"]);

assert_eq!(diagnostic.expected, ["right", "down"]);
}
}
157 changes: 101 additions & 56 deletions src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@ pub(crate) fn tokenize(source: &str) -> LexResult<Vec<Token>> {
line: 1,
column: 2,
};
return Err(Box::new(Diagnostic::error(
"STK1002",
"A byte order mark is not permitted.",
Span { start, end },
)));
return Err(Box::new(
Diagnostic::error(
"STK1002",
"A byte order mark is not permitted.",
Span { start, end },
)
.with_help("Remove the UTF-8 byte order mark at the start of the file."),
));
}

Lexer::new(source).tokenize()
Expand Down Expand Up @@ -131,14 +134,18 @@ impl<'source> Lexer<'source> {

loop {
let Some(character) = self.peek() else {
return Err(Box::new(Diagnostic::error(
"STK2003",
"Input ended before the string was closed.",
Span {
start,
end: self.position,
},
)));
return Err(Box::new(
Diagnostic::error(
"STK2003",
"Input ended before the string was closed.",
Span {
start,
end: self.position,
},
)
.with_expected(["\""])
.with_help("Add a closing double quote."),
));
};

match character {
Expand All @@ -162,22 +169,30 @@ impl<'source> Lexer<'source> {
start: self.position,
end: self.position_after_current(),
};
return Err(Box::new(Diagnostic::error(
"STK1003",
"Strings cannot contain line breaks or tabs.",
span,
)));
return Err(Box::new(
Diagnostic::error(
"STK1003",
"Strings cannot contain line breaks or tabs.",
span,
)
.with_help(
"Remove the line break or tab; Stack strings must stay on one line.",
),
));
}
value_character if value_character.is_control() => {
let span = Span {
start: self.position,
end: self.position_after_current(),
};
return Err(Box::new(Diagnostic::error(
"STK1003",
"Strings cannot contain control characters.",
span,
)));
return Err(Box::new(
Diagnostic::error(
"STK1003",
"Strings cannot contain control characters.",
span,
)
.with_help("Remove the control character from the string."),
));
}
value_character => {
value.push(value_character);
Expand Down Expand Up @@ -257,14 +272,18 @@ impl<'source> Lexer<'source> {
fn lex_escape(&mut self, value: &mut String, string_start: SourcePosition) -> LexResult<()> {
let escape_start = self.position;
let Some(character) = self.peek() else {
return Err(Box::new(Diagnostic::error(
"STK1003",
"Input ended inside a string escape.",
Span {
start: string_start,
end: self.position,
},
)));
return Err(Box::new(
Diagnostic::error(
"STK1003",
"Input ended inside a string escape.",
Span {
start: string_start,
end: self.position,
},
)
.with_expected([r#"\""#, r#"\\"#, r#"\uXXXX"#])
.with_help("Complete the escape using a supported Stack string escape."),
));
};

match character {
Expand Down Expand Up @@ -303,14 +322,19 @@ impl<'source> Lexer<'source> {
return Err(self.invalid_escape(escape_start));
};
if decoded.is_control() || matches!(decoded, '\n' | '\r' | '\t') {
return Err(Box::new(Diagnostic::error(
"STK1003",
"A string escape decoded to a prohibited control value.",
Span {
start: escape_start,
end: self.position,
},
)));
return Err(Box::new(
Diagnostic::error(
"STK1003",
"A string escape decoded to a prohibited control value.",
Span {
start: escape_start,
end: self.position,
},
)
.with_help(
"Use a printable Unicode scalar that is not a line break or tab.",
),
));
}
value.push(decoded);
}
Expand All @@ -337,25 +361,32 @@ impl<'source> Lexer<'source> {
}

fn invalid_escape(&self, start: SourcePosition) -> Box<Diagnostic> {
Box::new(Diagnostic::error(
"STK1003",
"The string contains an invalid escape.",
Span {
start,
end: self.position,
},
))
Box::new(
Diagnostic::error(
"STK1003",
"The string contains an invalid escape.",
Span {
start,
end: self.position,
},
)
.with_expected([r#"\""#, r#"\\"#, r#"\uXXXX"#])
.with_help("Use an escaped double quote, backslash, or Unicode code unit."),
)
}

fn invalid_surrogate(&self, start: SourcePosition) -> Box<Diagnostic> {
Box::new(Diagnostic::error(
"STK1003",
"The string contains an unpaired Unicode surrogate.",
Span {
start,
end: self.position,
},
))
Box::new(
Diagnostic::error(
"STK1003",
"The string contains an unpaired Unicode surrogate.",
Span {
start,
end: self.position,
},
)
.with_help("Pair high and low surrogates or use a non-surrogate Unicode code unit."),
)
}

fn lex_bare(&mut self) -> TokenKind {
Expand Down Expand Up @@ -512,12 +543,26 @@ mod tests {
let result = tokenize(source);
assert!(matches!(result, Err(diagnostic) if diagnostic.code == "STK1003"));
}

let invalid_escape = tokenize(r#""\n""#);
assert!(matches!(
invalid_escape,
Err(diagnostic)
if diagnostic.expected == [r#"\""#, r#"\\"#, r#"\uXXXX"#]
&& diagnostic.help.is_some()
));
}

#[test]
fn reports_an_unterminated_string() {
let result = tokenize("\"unfinished");
assert!(matches!(result, Err(diagnostic) if diagnostic.code == "STK2003"));
assert!(matches!(
result,
Err(diagnostic)
if diagnostic.code == "STK2003"
&& diagnostic.expected == ["\""]
&& diagnostic.help.as_deref() == Some("Add a closing double quote.")
));
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ fn invalid_utf8_diagnostic(source: &[u8], error: std::str::Utf8Error) -> Diagnos
"Input is not valid UTF-8.",
diagnostic::Span::point(position),
)
.with_help("Save the source as UTF-8 and replace the invalid byte sequence.")
}

#[cfg(test)]
Expand Down
Loading