#![forbid(unsafe_code)]
pub mod ast;
pub mod dialect;
pub mod tokens;
pub mod visit;
use plsql_core::{Diagnostic, FileId};
use serde::{Deserialize, Serialize};
use tracing::instrument;
pub use dialect::{
UNSUPPORTED_DIALECT_FEATURE_CODE, unsupported_dialect_feature_diagnostic,
unsupported_dialect_feature_remediation,
};
pub use ast::{
Ast, AstDecl, AstExpr, AstStatement, AstTypeDecl, ConcreteSyntaxTree, CstNodeId, SourceFile,
SourceMap, Spanned,
};
pub use tokens::{Token, TokenKind, TokenTape, Trivia, TriviaTable};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ParseOptions {
pub oracle_version: OracleTargetVersion,
pub recovery: RecoveryMode,
}
impl Default for ParseOptions {
fn default() -> Self {
Self {
oracle_version: OracleTargetVersion::Oracle19c,
recovery: RecoveryMode::RecoverAtStatementBoundary,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum OracleTargetVersion {
Oracle11g,
Oracle12c,
#[default]
Oracle19c,
Oracle21c,
Oracle23ai,
Oracle26ai,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum RecoveryMode {
FailFast,
#[default]
RecoverAtStatementBoundary,
AggressiveRecovery,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct ParseMetrics {
pub total_tokens: u64,
pub trivia_count: u64,
pub diagnostic_count: u64,
pub recovery_count: u64,
pub source_bytes: u64,
}
#[derive(Debug)]
pub struct BackendParseResult {
pub cst: ConcreteSyntaxTree,
pub ast: Ast,
pub diagnostics: Vec<Diagnostic>,
pub metrics: ParseMetrics,
pub recovered: bool,
}
#[derive(Debug)]
pub struct ParseResult {
pub file_id: FileId,
pub cst: ConcreteSyntaxTree,
pub ast: Ast,
pub diagnostics: Vec<Diagnostic>,
pub metrics: ParseMetrics,
pub recovered: bool,
}
impl ParseResult {
#[must_use]
#[instrument(level = "trace", skip(self))]
pub fn is_clean(&self) -> bool {
!self
.diagnostics
.iter()
.any(|d| d.severity >= plsql_core::Severity::Error)
}
#[must_use]
#[instrument(level = "trace", skip(self))]
pub fn was_recovered(&self) -> bool {
self.recovered
}
}
pub trait ParseBackend: Send + Sync {
fn name(&self) -> &'static str;
fn parse(&self, input: &str, file_id: FileId, opts: &ParseOptions) -> BackendParseResult;
}
#[instrument(level = "debug", skip(backend, opts))]
pub fn parse_with_backend<B: ParseBackend>(
input: &str,
file_id: FileId,
backend: &B,
opts: &ParseOptions,
) -> ParseResult {
let span = tracing::info_span!("parse_with_backend", backend = backend.name());
let _enter = span.enter();
let backend_result = backend.parse(input, file_id, opts);
ParseResult {
file_id,
cst: backend_result.cst,
ast: backend_result.ast,
diagnostics: backend_result.diagnostics,
metrics: backend_result.metrics,
recovered: backend_result.recovered,
}
}
#[instrument(level = "debug", skip(backend))]
pub fn parse_file<B: ParseBackend>(input: &str, file_id: FileId, backend: &B) -> ParseResult {
parse_with_backend(input, file_id, backend, &ParseOptions::default())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_options_default_is_19c_with_recovery() {
let opts = ParseOptions::default();
assert_eq!(opts.oracle_version, OracleTargetVersion::Oracle19c);
assert_eq!(opts.recovery, RecoveryMode::RecoverAtStatementBoundary);
}
#[test]
fn parse_options_round_trips_through_json() {
let opts = ParseOptions::default();
let json = serde_json::to_string(&opts).unwrap();
let back: ParseOptions = serde_json::from_str(&json).unwrap();
assert_eq!(back.oracle_version, OracleTargetVersion::Oracle19c);
assert_eq!(back.recovery, RecoveryMode::RecoverAtStatementBoundary);
}
#[test]
fn parse_metrics_default_is_zero() {
let m = ParseMetrics::default();
assert_eq!(m.total_tokens, 0);
assert_eq!(m.trivia_count, 0);
assert_eq!(m.diagnostic_count, 0);
assert_eq!(m.recovery_count, 0);
assert_eq!(m.source_bytes, 0);
}
struct RecordingBackend {
seen_opts: std::sync::Mutex<Vec<ParseOptions>>,
}
impl RecordingBackend {
fn new() -> Self {
Self {
seen_opts: std::sync::Mutex::new(Vec::new()),
}
}
}
impl ParseBackend for RecordingBackend {
fn name(&self) -> &'static str {
"recording"
}
fn parse(&self, input: &str, _file_id: FileId, opts: &ParseOptions) -> BackendParseResult {
self.seen_opts
.lock()
.expect("opts mutex poisoned")
.push(opts.clone());
BackendParseResult {
cst: ConcreteSyntaxTree::new(),
ast: Ast::new(),
diagnostics: Vec::new(),
metrics: ParseMetrics {
source_bytes: input.len() as u64,
..ParseMetrics::default()
},
recovered: false,
}
}
}
#[test]
fn parse_file_forwards_default_parse_options() {
let backend = RecordingBackend::new();
let _ = parse_file("BEGIN NULL; END;", FileId::new(1), &backend);
let seen = backend.seen_opts.lock().expect("opts mutex poisoned");
assert_eq!(seen.len(), 1, "backend must be invoked exactly once");
assert_eq!(seen[0].oracle_version, OracleTargetVersion::Oracle19c);
assert_eq!(seen[0].recovery, RecoveryMode::RecoverAtStatementBoundary);
}
#[test]
fn parse_file_pairs_result_with_its_file_id() {
let backend = RecordingBackend::new();
let result = parse_file("SELECT 1 FROM dual;", FileId::new(42), &backend);
assert_eq!(result.file_id, FileId::new(42));
}
#[test]
fn parse_file_propagates_backend_metrics() {
let backend = RecordingBackend::new();
let input = "CREATE PACKAGE p IS END;";
let result = parse_file(input, FileId::new(7), &backend);
assert_eq!(result.metrics.source_bytes, input.len() as u64);
assert!(
result.is_clean(),
"a clean recording parse carries no error diagnostics"
);
assert!(!result.was_recovered());
}
#[test]
fn parse_file_handles_empty_input_without_panicking() {
let backend = RecordingBackend::new();
let result = parse_file("", FileId::new(0), &backend);
assert_eq!(result.metrics.source_bytes, 0);
assert_eq!(result.file_id, FileId::new(0));
}
}