use super::diagnostics::{
bare_feature_declaration_in_part_def_diagnostic, category_from_code,
invalid_bare_identifier_in_body_diagnostic, invalid_expose_separator_diagnostic,
invalid_typing_operator_diagnostic, invalid_unit_reference_diagnostic,
missing_expression_after_operator_diagnostic, missing_name_diagnostic,
missing_semicolon_or_body_diagnostic, missing_type_diagnostic, trim_ascii_end,
trim_ascii_start, unexpected_keyword_in_scope_diagnostic,
};
use super::lex;
use super::Input;
use crate::ast::ParseErrorNode;
use crate::error::{DiagnosticCategory, DiagnosticSeverity, ParseError};
pub(crate) fn recovery_found_snippet(input: Input<'_>) -> Option<String> {
let frag = input.fragment();
let take = frag
.iter()
.position(|&b| b == b'\n' || b == b'\r')
.unwrap_or(frag.len())
.min(60);
let snippet = String::from_utf8_lossy(&frag[..take]).trim().to_string();
if snippet.is_empty() {
None
} else {
Some(snippet)
}
}
pub(crate) fn recovery_found_snippet_from_span(
input: Input<'_>,
recovery_end: Input<'_>,
) -> Option<String> {
let consumed_len = recovery_end
.location_offset()
.saturating_sub(input.location_offset())
.min(input.fragment().len());
if consumed_len == 0 {
return recovery_found_snippet(input);
}
let frag = &input.fragment()[..consumed_len];
let take = frag
.iter()
.position(|&b| b == b'\n' || b == b'\r')
.unwrap_or(frag.len())
.min(60);
let snippet = String::from_utf8_lossy(&frag[..take]).trim().to_string();
if snippet.is_empty() {
recovery_found_snippet(input)
} else {
Some(snippet)
}
}
pub(crate) fn build_recovery_error_node(
input: Input<'_>,
starters: &[&[u8]],
scope_label: &str,
generic_code: &str,
) -> ParseErrorNode {
build_recovery_error_node_from_span(input, input, starters, scope_label, generic_code)
}
enum RecoveryClassification {
MissingMemberName {
code: String,
message: String,
expected: String,
suggestion: String,
},
MissingTypeReference {
code: String,
message: String,
expected: String,
suggestion: String,
},
InvalidQualifiedNameSeparator {
code: String,
message: String,
expected: String,
suggestion: String,
},
MissingBodyOrSemicolon {
code: String,
message: String,
expected: String,
suggestion: String,
},
BareFeatureDeclarationInPartDef {
code: String,
message: String,
expected: String,
suggestion: String,
},
MissingExpressionAfterOperator {
code: String,
message: String,
expected: String,
suggestion: String,
},
InvalidUnitReference {
code: String,
message: String,
expected: String,
suggestion: String,
},
InvalidTypingOperator {
code: String,
message: String,
expected: String,
suggestion: String,
},
InvalidBareIdentifierInBody {
code: String,
message: String,
expected: String,
suggestion: String,
},
UnexpectedKeywordInScope {
code: String,
message: String,
expected: String,
suggestion: String,
},
MissingSemicolon,
UnsupportedAnnotation,
Unexpected,
}
fn classify_recovery(
input: Input<'_>,
recovery_end: Input<'_>,
starters: &[&[u8]],
scope_label: &str,
) -> RecoveryClassification {
let trimmed = trim_ascii_start(input.fragment());
if let Some((code, message, expected, suggestion)) =
missing_name_diagnostic(trimmed, scope_label)
{
return RecoveryClassification::MissingMemberName {
code: code.to_string(),
message,
expected,
suggestion,
};
}
if let Some((code, message, expected, suggestion)) = missing_type_diagnostic(trimmed) {
return RecoveryClassification::MissingTypeReference {
code: code.to_string(),
message,
expected,
suggestion,
};
}
if let Some((code, message, expected, suggestion)) =
invalid_expose_separator_diagnostic(trimmed)
{
return RecoveryClassification::InvalidQualifiedNameSeparator {
code: code.to_string(),
message,
expected,
suggestion,
};
}
if let Some((code, message, expected, suggestion)) = invalid_typing_operator_diagnostic(trimmed)
{
return RecoveryClassification::InvalidTypingOperator {
code: code.to_string(),
message,
expected,
suggestion,
};
}
if let Some((code, message, expected, suggestion)) =
missing_expression_after_operator_diagnostic(trimmed)
{
return RecoveryClassification::MissingExpressionAfterOperator {
code: code.to_string(),
message,
expected,
suggestion,
};
}
if let Some((code, message, expected, suggestion)) = invalid_unit_reference_diagnostic(trimmed)
{
return RecoveryClassification::InvalidUnitReference {
code: code.to_string(),
message,
expected,
suggestion,
};
}
if scope_label.contains("part definition body") {
if let Some((code, message, expected, suggestion)) =
bare_feature_declaration_in_part_def_diagnostic(trimmed)
{
return RecoveryClassification::BareFeatureDeclarationInPartDef {
code: code.to_string(),
message,
expected,
suggestion,
};
}
}
if let Some((code, message, expected, suggestion)) =
missing_semicolon_or_body_diagnostic(trimmed)
{
return RecoveryClassification::MissingBodyOrSemicolon {
code: code.to_string(),
message,
expected,
suggestion,
};
}
let consumed_len = recovery_end
.location_offset()
.saturating_sub(input.location_offset())
.min(input.fragment().len());
let raw_consumed = &input.fragment()[..consumed_len];
let consumed = trim_ascii_end(raw_consumed);
let recovered_to_boundary = recovery_end.location_offset() > input.location_offset() && {
let (next, _) = lex::ws_and_comments(recovery_end).unwrap_or((recovery_end, ()));
next.fragment().is_empty()
|| next.fragment().starts_with(b"}")
|| lex::starts_with_any_keyword(next.fragment(), starters)
};
let consumed_has_newline = raw_consumed.contains(&b'\n') || raw_consumed.contains(&b'\r');
let first_line_end = consumed
.iter()
.position(|b| matches!(*b, b'\n' | b'\r'))
.unwrap_or(consumed.len());
let first_line = trim_ascii_end(&consumed[..first_line_end]);
let consumed_has_delimiters = consumed
.iter()
.any(|b| matches!(*b, b'{' | b'}' | b'(' | b')' | b'[' | b']'));
let consumed_ends_incomplete = first_line.last().is_some_and(|b| {
matches!(
*b,
b':' | b'=' | b',' | b'.' | b'+' | b'-' | b'*' | b'/' | b'>' | b'<' | b'|'
)
});
let first_line_has_semicolon = first_line.contains(&b';');
if recovered_to_boundary
&& lex::starts_with_any_keyword(trimmed, starters)
&& (consumed_has_newline || recovery_end.fragment().starts_with(b"}"))
&& !consumed.is_empty()
&& !consumed_has_delimiters
&& !consumed_ends_incomplete
&& !first_line_has_semicolon
{
return RecoveryClassification::MissingSemicolon;
}
if lex::starts_with_keyword(trimmed, b"#") || lex::starts_with_keyword(trimmed, b"@") {
return RecoveryClassification::UnsupportedAnnotation;
}
if let Some((code, message, expected, suggestion)) =
invalid_bare_identifier_in_body_diagnostic(trimmed, scope_label)
{
return RecoveryClassification::InvalidBareIdentifierInBody {
code: code.to_string(),
message,
expected,
suggestion,
};
}
if let Some((code, message, expected, suggestion)) =
unexpected_keyword_in_scope_diagnostic(trimmed, starters, scope_label)
{
return RecoveryClassification::UnexpectedKeywordInScope {
code: code.to_string(),
message,
expected,
suggestion,
};
}
RecoveryClassification::Unexpected
}
pub(crate) fn build_recovery_error_node_from_span(
input: Input<'_>,
recovery_end: Input<'_>,
starters: &[&[u8]],
scope_label: &str,
generic_code: &str,
) -> ParseErrorNode {
match classify_recovery(input, recovery_end, starters, scope_label) {
RecoveryClassification::MissingMemberName {
code,
message,
expected,
suggestion,
}
| RecoveryClassification::MissingTypeReference {
code,
message,
expected,
suggestion,
}
| RecoveryClassification::InvalidQualifiedNameSeparator {
code,
message,
expected,
suggestion,
}
| RecoveryClassification::MissingBodyOrSemicolon {
code,
message,
expected,
suggestion,
}
| RecoveryClassification::BareFeatureDeclarationInPartDef {
code,
message,
expected,
suggestion,
}
| RecoveryClassification::MissingExpressionAfterOperator {
code,
message,
expected,
suggestion,
}
| RecoveryClassification::InvalidUnitReference {
code,
message,
expected,
suggestion,
}
| RecoveryClassification::InvalidTypingOperator {
code,
message,
expected,
suggestion,
}
| RecoveryClassification::InvalidBareIdentifierInBody {
code,
message,
expected,
suggestion,
}
| RecoveryClassification::UnexpectedKeywordInScope {
code,
message,
expected,
suggestion,
} => ParseErrorNode {
message,
code,
expected: Some(expected),
found: recovery_found_snippet_from_span(input, recovery_end),
suggestion: Some(suggestion),
category: Some(DiagnosticCategory::ParseError),
},
RecoveryClassification::MissingSemicolon => ParseErrorNode {
message: "missing semicolon before next declaration".to_string(),
code: "missing_semicolon".to_string(),
expected: Some("';'".to_string()),
found: recovery_found_snippet_from_span(input, recovery_end),
suggestion: Some("Insert ';' before this declaration.".to_string()),
category: Some(DiagnosticCategory::ParseError),
},
RecoveryClassification::UnsupportedAnnotation => ParseErrorNode {
message: format!("unsupported annotation syntax in {scope_label}"),
code: "unsupported_annotation_syntax".to_string(),
expected: Some(format!("valid {scope_label} element")),
found: recovery_found_snippet_from_span(input, recovery_end),
suggestion: Some(
"Remove this annotation or extend the parser to support annotated declarations."
.to_string(),
),
category: Some(DiagnosticCategory::UnsupportedGrammarForm),
},
RecoveryClassification::Unexpected => ParseErrorNode {
message: format!("unexpected token in {scope_label}"),
code: generic_code.to_string(),
expected: Some(format!("valid {scope_label} element")),
found: recovery_found_snippet_from_span(input, recovery_end),
suggestion: Some(format!("Fix this {scope_label} member and re-run parsing.")),
category: Some(DiagnosticCategory::ParseError),
},
}
}
pub(crate) fn parse_error_from_recovery_node(
span: &crate::ast::Span,
node: &ParseErrorNode,
) -> ParseError {
let mut err = ParseError::new(node.message.clone())
.with_location(span.offset, span.line, span.column)
.with_length(span.len.max(1))
.with_code(node.code.clone())
.with_category(
node.category
.unwrap_or_else(|| category_from_code(node.code.as_str())),
);
let severity = if node.code == "unsupported_annotation_syntax" {
DiagnosticSeverity::Warning
} else {
DiagnosticSeverity::Error
};
err = err.with_severity(severity);
if let Some(expected) = &node.expected {
err = err.with_expected(expected.clone());
}
if let Some(found) = &node.found {
err = err.with_found(found.clone());
}
if let Some(suggestion) = &node.suggestion {
err = err.with_suggestion(suggestion.clone());
}
err
}