Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

Commit

Permalink
feat(rslint_parser): ParsedSyntax, ConditionalParsedSyntax, and Inval…
Browse files Browse the repository at this point in the history
…idParsedSyntax

Introduces the new `ParsedSyntax`, `ConditionalParsedSyntax`, and `InvalidParsedSyntax` that all require explicit error handling. 

See #1815
  • Loading branch information
MichaReiser committed Nov 25, 2021
1 parent cbbdd0e commit 22cd68b
Show file tree
Hide file tree
Showing 30 changed files with 1,307 additions and 385 deletions.
96 changes: 94 additions & 2 deletions crates/rslint_parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ mod lossless_tree_sink;
mod lossy_tree_sink;
mod numbers;
mod parse;
pub(crate) mod parse_recovery;
mod state;
mod syntax_node;
mod token_source;
Expand All @@ -84,7 +83,7 @@ pub use crate::{
lossy_tree_sink::LossyTreeSink,
numbers::BigInt,
parse::*,
parser::{Checkpoint, CompletedMarker, Marker, Parser},
parser::{Checkpoint, CompletedMarker, Marker, ParseRecovery, Parser},
state::{ParserState, StrictMode},
syntax_node::*,
token_set::TokenSet,
Expand All @@ -100,6 +99,8 @@ pub use rslint_syntax::*;
/// It also includes labels and possibly notes
pub type ParserError = rslint_errors::Diagnostic;

use crate::parser::{ConditionalParsedSyntax, ParsedSyntax};
use rslint_errors::Diagnostic;
use std::ops::Range;

/// Abstracted token for `TokenSource`
Expand Down Expand Up @@ -246,3 +247,94 @@ impl From<FileKind> for Syntax {
Syntax::new(kind)
}
}

/// A syntax feature that may or may not be supported depending on the file type and parser configuration
pub trait SyntaxFeature: Sized {
/// Returns `true` if the current parsing context supports this syntax feature.
fn is_supported(&self, p: &Parser) -> bool;

/// Returns `true` if the current parsing context doesn't support this syntax feature.
fn is_unsupported(&self, p: &Parser) -> bool {
!self.is_supported(p)
}

/// Creates a syntax that is only valid if this syntax feature is supported in the current
/// parsing context, adds a diagnostic if not.
///
/// Returns [Valid] if this syntax feature is supported.
///
/// Returns [Invalid], creates a diagnostic with the passed in error builder,
/// and adds it to the parsing diagnostics if this syntax feature isn't supported.
fn exclusive_syntax<S, E>(
&self,
p: &mut Parser,
syntax: S,
error_builder: E,
) -> ConditionalParsedSyntax
where
S: Into<ParsedSyntax>,
E: FnOnce(&Parser, &CompletedMarker) -> Diagnostic,
{
syntax.into().exclusive_for(self, p, error_builder)
}

/// Creates a syntax that is only valid if this syntax feature is supported in the current
/// parsing context.
///
/// Returns [Valid] if this syntax feature is supported and [Invalid] if this syntax isn't supported.
fn exclusive_syntax_no_error<S>(&self, p: &Parser, syntax: S) -> ConditionalParsedSyntax
where
S: Into<ParsedSyntax>,
{
syntax.into().exclusive_for_no_error(self, p)
}

/// Creates a syntax that is only valid if the current parsing context doesn't support this syntax feature,
/// and adds a diagnostic if it does.
///
/// Returns [Valid] if the parsing context doesn't support this syntax feature
///
/// Creates a diagnostic using the passed error builder, adds it to the parsing diagnostics, and returns
/// [Invalid] if the parsing context does support this syntax feature.
fn excluding_syntax<S, E>(
&self,
p: &mut Parser,
syntax: S,
error_builder: E,
) -> ConditionalParsedSyntax
where
S: Into<ParsedSyntax>,
E: FnOnce(&Parser, &CompletedMarker) -> Diagnostic,
{
syntax.into().excluding(self, p, error_builder)
}

/// Creates a syntax that is only valid if this syntax feature isn't supported in the current
/// parsing context.
///
/// Returns [Valid] if this syntax feature isn't supported and [Invalid] if it is.
fn excluding_syntax_no_error<S>(&self, p: &Parser, syntax: S) -> ConditionalParsedSyntax
where
S: Into<ParsedSyntax>,
{
syntax.into().excluding_no_error(self, p)
}
}

pub enum JsSyntaxFeature {
#[allow(unused)]
#[doc(alias = "LooseMode")]
SloppyMode,
StrictMode,
TypeScript,
}

impl SyntaxFeature for JsSyntaxFeature {
fn is_supported(&self, p: &Parser) -> bool {
match self {
JsSyntaxFeature::SloppyMode => p.state.strict.is_none(),
JsSyntaxFeature::StrictMode => p.state.strict.is_some(),
JsSyntaxFeature::TypeScript => p.syntax.file_kind == FileKind::TypeScript,
}
}
}
32 changes: 28 additions & 4 deletions crates/rslint_parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@
//! the parser yields events like `Start node`, `Error`, etc.
//! These events are then applied to a `TreeSink`.

pub(crate) mod parse_error;
mod parse_recovery;
mod parsed_syntax;
pub(crate) mod single_token_parse_recovery;

use drop_bomb::DropBomb;
use rslint_errors::Diagnostic;
use rslint_syntax::SyntaxKind::EOF;
use std::borrow::BorrowMut;
use std::cell::Cell;
use std::ops::Range;

pub use parse_error::*;
pub use parsed_syntax::{ConditionalParsedSyntax, InvalidParsedSyntax, ParsedSyntax};
#[allow(deprecated)]
pub use single_token_parse_recovery::SingleTokenParseRecovery;

pub use crate::parser::parse_recovery::{ParseRecovery, RecoveryError, RecoveryResult};
use crate::*;

/// An extremely fast, error tolerant, completely lossless JavaScript parser
Expand Down Expand Up @@ -273,7 +284,7 @@ impl<'t> Parser<'t> {
.expect("Parser source and tokens mismatch")
}

/// Try to eat a specific token kind, if the kind is not there then add a missing marker and add an error to the events stack.
/// Try to eat a specific token kind, if the kind is not there then adds an error to the events stack.
pub fn expect(&mut self, kind: SyntaxKind) -> bool {
if self.eat(kind) {
true
Expand All @@ -297,12 +308,21 @@ impl<'t> Parser<'t> {
.primary(self.cur_tok().range, "unexpected")
};

self.missing();
self.error(err);
false
}
}

/// Tries to eat a specific token kind, adds a missing marker and an error to the events stack if it's not there.
pub fn expect_required(&mut self, kind: SyntaxKind) -> bool {
if !self.expect(kind) {
self.missing();
false
} else {
true
}
}

/// Get the byte index range of a completed marker for error reporting.
pub fn marker_range(&self, marker: &CompletedMarker) -> Range<usize> {
match self.events[marker.start_pos as usize] {
Expand All @@ -318,6 +338,7 @@ impl<'t> Parser<'t> {
///
/// # Panics
/// Panics if the AST node represented by the marker does not match the generic
#[deprecated(note = "Unsafe and fairly expensive.")]
pub fn parse_marker<T: AstNode>(&self, marker: &CompletedMarker) -> T {
let events = self
.events
Expand Down Expand Up @@ -428,7 +449,7 @@ impl<'t> Parser<'t> {
if self.state.no_recovery {
Some(true).filter(|_| self.eat(kind))
} else {
Some(self.expect(kind))
Some(self.expect_required(kind))
}
}

Expand All @@ -455,6 +476,9 @@ impl<'t> Parser<'t> {
start..end
}

#[deprecated(
note = "Use ParseRecovery instead which signals with a Result if the recovery was successful or not"
)]
pub fn expr_with_semi_recovery(&mut self, assign: bool) -> Option<CompletedMarker> {
let func = if assign {
syntax::expr::assign_expr
Expand Down Expand Up @@ -725,7 +749,7 @@ mod tests {
let mut p = Parser::new(token_source, 0, Syntax::default());

let m = p.start();
p.expect(SyntaxKind::JS_STRING_LITERAL);
p.expect_required(SyntaxKind::JS_STRING_LITERAL);
m.complete(&mut p, SyntaxKind::JS_STRING_LITERAL_EXPRESSION);
}

Expand Down
91 changes: 91 additions & 0 deletions crates/rslint_parser/src/parser/parse_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use crate::Parser;
use rslint_errors::{Diagnostic, Span};
use std::ops::Range;

///! Provides helper functions to build common diagnostic messages

/// Creates a diagnostic saying that the node [name] was expected at range
pub(crate) fn expected_node(name: &str, range: Range<usize>) -> ExpectedNodeDiagnosticBuilder {
ExpectedNodeDiagnosticBuilder::with_single_node(name, range)
}

/// Creates a diagnostic saying that any of the nodes in [names] was expected at range
pub(crate) fn expected_any(names: &[&str], range: Range<usize>) -> ExpectedNodeDiagnosticBuilder {
ExpectedNodeDiagnosticBuilder::with_any(names, range)
}

pub trait ToDiagnostic {
fn to_diagnostic(&self, p: &Parser) -> Diagnostic;
}

pub struct ExpectedNodeDiagnosticBuilder {
names: String,
range: Range<usize>,
}

impl ExpectedNodeDiagnosticBuilder {
fn with_single_node(name: &str, range: Range<usize>) -> Self {
ExpectedNodeDiagnosticBuilder {
names: format!("{} {}", article_for(name), name),
range,
}
}

fn with_any(names: &[&str], range: Range<usize>) -> Self {
debug_assert!(names.len() > 1, "Requires at least 2 names");

if names.len() < 2 {
return Self::with_single_node(names.first().unwrap_or(&"<missing>"), range);
}

let mut joined_names = String::new();

for (index, name) in names.iter().enumerate() {
if index > 0 {
joined_names.push_str(", ");
}

if index == names.len() - 1 {
joined_names.push_str("or ");
}

joined_names.push_str(article_for(name));
joined_names.push(' ');
joined_names.push_str(name);
}

Self {
names: joined_names,
range,
}
}
}

impl ToDiagnostic for ExpectedNodeDiagnosticBuilder {
fn to_diagnostic(&self, p: &Parser) -> Diagnostic {
let range = &self.range;

let msg = if range.is_empty() && p.tokens.source().get(range.to_owned()) == None {
format!(
"expected {} but instead found the end of the file",
self.names
)
} else {
format!(
"expected {} but instead found '{}'",
self.names,
p.source(range.as_text_range())
)
};

let diag = p.err_builder(&msg);
diag.primary(&self.range, format!("Expected {} here", self.names))
}
}

fn article_for(name: &str) -> &'static str {
match name.chars().next() {
Some('a' | 'e' | 'i' | 'o' | 'u') => "an",
_ => "a",
}
}
90 changes: 90 additions & 0 deletions crates/rslint_parser/src/parser/parse_recovery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use crate::{CompletedMarker, Parser, TokenSet};
use rslint_syntax::SyntaxKind;
use rslint_syntax::SyntaxKind::EOF;
use std::error::Error;
use std::fmt::{Display, Formatter};

#[derive(Debug)]
pub enum RecoveryError {
/// Recovery failed because the parser reached the end of file
Eof,

/// Recovery failed because it didn't eat any tokens. Meaning, the parser is already in a recovered state.
/// This is an error because:
/// a) It shouldn't create a completed marker wrapping no tokens
/// b) This results in an infinite-loop if the recovery is used inside of a while loop. For example,
/// it's common that list parsing also recovers at the end of a statement or block. However, list elements
/// don't start with a `;` or `}` which is why parsing, for example, an array element fails again and
/// the array expression triggers another recovery. Handling this as an error ensures that list parsing
/// rules break out of the loop the same way as they would at the EOF.
AlreadyRecovered,
}

impl Error for RecoveryError {}

impl Display for RecoveryError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
RecoveryError::Eof => write!(f, "EOF"),
RecoveryError::AlreadyRecovered => write!(f, "already recovered"),
}
}
}

pub type RecoveryResult = Result<CompletedMarker, RecoveryError>;

/// Recovers the parser by finding a token/point (depending on the configuration) from where
/// the caller knows how to proceed parsing. The recovery wraps all the skipped tokens inside of an `Unknown` node.
/// A safe recovery point for an array element could by finding the next `,` or `]`.
pub struct ParseRecovery {
node_kind: SyntaxKind,
recovery_set: TokenSet,
line_break: bool,
}

impl ParseRecovery {
/// Creates a new parse recovery that eats all tokens until it finds any token in the passed recovery set.
pub fn new(node_kind: SyntaxKind, recovery_set: TokenSet) -> Self {
Self {
node_kind,
recovery_set,
line_break: false,
}
}

/// Enable recovery on line breaks
pub fn enable_recovery_on_line_break(mut self) -> Self {
self.line_break = true;
self
}

// TODO: Add a `recover_until` which recovers until the parser reached a token inside of the recovery set
// or the passed in `parse_*` rule was able to successfully parse an element.

/// Tries to recover by parsing all tokens into an `Unknown*` node until the parser finds any token
/// specified in the recovery set, the EOF, or a line break (depending on configuration).
/// Returns `Ok(unknown_node)` if recovery was successful, and `Err(RecoveryError::Eof)` if the parser
/// is at the end of the file (before starting recovery).
pub fn recover(&self, p: &mut Parser) -> RecoveryResult {
if p.at(EOF) {
return Err(RecoveryError::Eof);
}

if self.recovered(p) {
return Err(RecoveryError::AlreadyRecovered);
}

let m = p.start();

while !self.recovered(p) {
p.bump_any();
}

Ok(m.complete(p, self.node_kind))
}

#[inline]
fn recovered(&self, p: &Parser) -> bool {
p.at_ts(self.recovery_set) || p.at(EOF) || (self.line_break && p.has_linebreak_before_n(0))
}
}

0 comments on commit 22cd68b

Please sign in to comment.