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
114 changes: 61 additions & 53 deletions crates/pine-lexer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,37 @@ use pine_core::PineVersion;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum LexerError {
#[error("Unterminated string at line {line}, column {column}")]
UnterminatedString { line: usize, column: usize },

#[error("Invalid hex color format '{value}' at line {line}, column {column}")]
InvalidHexColor {
value: String,
line: usize,
column: usize,
},

#[error("Unexpected character '{ch}' at line {line}, column {column}")]
UnexpectedCharacter {
ch: char,
line: usize,
column: usize,
},

#[error("Indentation error at line {line}")]
IndentationError { line: usize },

#[error("Invalid number '{value}' at line {line}, column {column}")]
InvalidNumber {
value: String,
line: usize,
column: usize,
},
pub enum LexerErrorKind {
#[error("Unterminated string")]
UnterminatedString,

#[error("Invalid hex color format '{value}'")]
InvalidHexColor { value: String },

#[error("Unexpected character '{ch}'")]
UnexpectedCharacter { ch: char },

#[error("Indentation error")]
IndentationError,

#[error("Invalid number '{value}'")]
InvalidNumber { value: String },
}

/// A lexing error and the 1-based source position it points at.
#[derive(Debug, Error)]
#[error("{kind} at line {line}, column {column}")]
pub struct LexerError {
pub line: usize,
pub column: usize,
pub kind: LexerErrorKind,
}

impl LexerError {
/// The 1-based `(line, column)` of the offending character.
pub fn location(&self) -> (u32, u32) {
(self.line as u32, self.column as u32)
}
}

// Token types
Expand Down Expand Up @@ -210,24 +214,23 @@ impl Lexer {
// No decimal point means an integer literal; Pine treats the two types
// differently. (This lexer does not read scientific notation, so a `.`
// is the only thing that makes a literal a float.)
let typ =
if num_str.contains('.') {
TokenType::Number(num_str.parse::<f64>().map_err(|_| {
LexerError::InvalidNumber {
value: num_str.clone(),
line: start_line,
column: start_col,
}
})?)
} else {
TokenType::IntLiteral(num_str.parse::<i64>().map_err(|_| {
LexerError::InvalidNumber {
value: num_str.clone(),
line: start_line,
column: start_col,
}
})?)
};
let typ = if num_str.contains('.') {
TokenType::Number(num_str.parse::<f64>().map_err(|_| LexerError {
line: start_line,
column: start_col,
kind: LexerErrorKind::InvalidNumber {
value: num_str.clone(),
},
})?)
} else {
TokenType::IntLiteral(num_str.parse::<i64>().map_err(|_| LexerError {
line: start_line,
column: start_col,
kind: LexerErrorKind::InvalidNumber {
value: num_str.clone(),
},
})?)
};
Ok(Token {
typ,
lexeme: num_str,
Expand Down Expand Up @@ -324,9 +327,10 @@ impl Lexer {
}
}

Err(LexerError::UnterminatedString {
Err(LexerError {
line: start_line,
column: start_col,
kind: LexerErrorKind::UnterminatedString,
})
}

Expand All @@ -350,10 +354,10 @@ impl Lexer {
// Validate length (should be 6 or 8 hex digits after #)
let hex_len = hex.len() - 1;
if hex_len != 6 && hex_len != 8 {
return Err(LexerError::InvalidHexColor {
value: hex,
return Err(LexerError {
line: start_line,
column: start_col,
kind: LexerErrorKind::InvalidHexColor { value: hex },
});
}

Expand Down Expand Up @@ -519,10 +523,10 @@ impl Lexer {
column: col,
}
} else {
return Err(LexerError::UnexpectedCharacter {
ch: '!',
return Err(LexerError {
line,
column: col,
kind: LexerErrorKind::UnexpectedCharacter { ch: '!' },
});
}
}
Expand Down Expand Up @@ -668,10 +672,10 @@ impl Lexer {
_ if ch.is_numeric() => return self.scan_number(),
_ if ch.is_alphabetic() || ch == '_' => self.scan_identifier(),
_ => {
return Err(LexerError::UnexpectedCharacter {
ch,
return Err(LexerError {
line,
column: col,
kind: LexerErrorKind::UnexpectedCharacter { ch },
})
}
};
Expand Down Expand Up @@ -829,7 +833,11 @@ impl Lexer {
// Check for indentation error
// SAFETY: indent_stack always has at least one element
if *self.indent_stack.last().unwrap() != indent_level {
return Err(LexerError::IndentationError { line });
return Err(LexerError {
line,
column: col,
kind: LexerErrorKind::IndentationError,
});
}
}
}
Expand Down
25 changes: 15 additions & 10 deletions crates/pine-lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ impl Backend {
.collect(),
Some(analysis.symbols),
),
// A lex/parse/version error stops analysis before any position is known.
Err(err) => (vec![error_diagnostic(&err)], None),
// A lex/parse/version error stops analysis; publish it as a single diagnostic.
Err(err) => (vec![error_diagnostic(&err, &text)], None),
};
self.documents
.lock()
Expand Down Expand Up @@ -243,9 +243,13 @@ fn to_lsp(diagnostic: &PineDiagnostic, text: &str) -> Diagnostic {
}
}

fn error_diagnostic(err: &pine_lang::Error) -> Diagnostic {
fn error_diagnostic(err: &pine_lang::Error, text: &str) -> Diagnostic {
let range = match err.location() {
Some((line, col)) => token_range(text, line, col),
None => Range::default(),
};
Diagnostic {
range: Range::new(Position::new(0, 0), Position::new(0, 0)),
range,
severity: Some(DiagnosticSeverity::ERROR),
source: Some("pinecone".to_string()),
message: err.to_string(),
Expand Down Expand Up @@ -314,14 +318,15 @@ mod tests {
}

#[test]
fn parse_error_becomes_a_single_error() {
let Err(err) = pine_lang::analyze("indicator(\n", None) else {
fn parse_error_points_at_its_line() {
let source = "//@version=6\nindicator(\"x\")\nlog.info(str.tostring. (up))\n";
let Err(err) = pine_lang::analyze(source, None) else {
panic!("expected a parse error");
};
assert_eq!(
error_diagnostic(&err).severity,
Some(DiagnosticSeverity::ERROR)
);
let diag = error_diagnostic(&err, source);
assert_eq!(diag.severity, Some(DiagnosticSeverity::ERROR));
// Points at the offending `up` token, not the top of the file.
assert_eq!(diag.range.start, Position::new(2, 24));
}

#[test]
Expand Down
Loading
Loading