Skip to content

Commit

Permalink
feat(transpiler): improve generated TypeScript output (#85)
Browse files Browse the repository at this point in the history
Signed-off-by: KieranKaelin <[email protected]>
  • Loading branch information
KieranKaelin authored May 25, 2023
1 parent f8a70a9 commit 75ed336
Show file tree
Hide file tree
Showing 10 changed files with 976 additions and 990 deletions.
7 changes: 3 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@ logos = "^0.12.1"
num-derive = "^0.3.3"
num-traits = "^0.2.15"
rowan = "^0.15.10"
serde-wasm-bindgen = "^0.4.5"
serde_repr = "^0.1.9"
serde-wasm-bindgen = "^0.5.0"
text-size = "^1.1.0"
thiserror = "^1.0.35"
tsify = "0.4.3"
unicode-width = "0.1.10"
wasm-bindgen = "^0.2.83"
wasm-typescript-definition = "^0.1.4"
wasm-bindgen = "^0.2.86"

[dependencies.serde]
version = "^1.0"
Expand Down
73 changes: 37 additions & 36 deletions src/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,22 @@
use std::collections::HashMap;

use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use tsify::Tsify;
use wasm_bindgen::prelude::*;
use wasm_typescript_definition::TypescriptDefinition;

use crate::ast::{AstNode, Root};
use crate::parser::*;
use crate::rules::{find_applicable_rules, RuleHint};
use crate::syntax::SyntaxKind;
use crate::util::SqlIdent;
use crate::SqlIdent;

/// Different types the analyzer can possibly examine.
///
/// Some types may be only available for specific frontends, e.g.
/// [`Package`][`DboType::Package`] is only available for Oracle databases.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize_repr)]
#[repr(usize)]
#[wasm_bindgen]
#[derive(Tsify, Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[serde(rename_all = "camelCase")]
pub enum DboType {
CheckConstraint,
DefaultExpr,
Expand All @@ -36,23 +35,26 @@ pub enum DboType {
View,
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize, TypescriptDefinition)]
#[derive(Tsify, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[serde(rename_all = "camelCase")]
pub struct DboFunctionMetaData {
pub name: String,
pub body: String,
pub lines_of_code: usize,
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize, TypescriptDefinition)]
#[derive(Tsify, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[serde(rename_all = "camelCase")]
pub struct DboProcedureMetaData {
pub name: String,
pub body: String,
pub lines_of_code: usize,
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize, TypescriptDefinition)]
#[derive(Tsify, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[serde(rename_all = "camelCase")]
pub struct DboQueryMetaData {
// For now, we only report how many OUTER JOINs there are, but not any
Expand All @@ -61,22 +63,25 @@ pub struct DboQueryMetaData {
}

/// The result of parsing and analyzing a piece of SQL code.
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, TypescriptDefinition)]
#[serde(rename_all = "camelCase")]
#[derive(Tsify, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct DboMetaData {
pub rules: Vec<RuleHint>,
#[serde(skip_serializing_if = "Option::is_none")]
pub function: Option<DboFunctionMetaData>,
#[serde(skip_serializing_if = "Option::is_none")]
pub procedure: Option<DboProcedureMetaData>,
#[serde(skip_serializing_if = "Option::is_none")]
pub query: Option<DboQueryMetaData>,
}

/// List of possible datatypes for tuple fields.
///
/// Mainly derived from <https://www.postgresql.org/docs/current/datatype.html>,
/// but furter extensible as needed. Keep alphabetically sorted.
#[derive(Copy, Clone, Debug, Eq, PartialEq, Deserialize_repr)]
#[repr(usize)]
#[wasm_bindgen]
#[derive(Tsify, Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[serde(rename_all = "camelCase")]
pub enum DboColumnType {
BigInt,
Date,
Expand All @@ -91,7 +96,8 @@ pub enum DboColumnType {
TimeWithTz,
}

#[derive(Clone, Debug, Eq, PartialEq, Deserialize, TypescriptDefinition)]
#[derive(Tsify, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
#[serde(rename_all = "camelCase")]
pub struct DboTableColumn {
typ: DboColumnType,
Expand All @@ -103,8 +109,10 @@ impl DboTableColumn {
}
}

#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
#[derive(Tsify, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct DboTable {
#[tsify(type = "Record<string, DboTableColumn>")]
columns: HashMap<SqlIdent, DboTableColumn>,
}

Expand All @@ -114,8 +122,10 @@ impl DboTable {
}
}

#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
#[derive(Tsify, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct DboAnalyzeContext {
#[tsify(type = "Record<string, DboTable>")]
tables: HashMap<SqlIdent, DboTable>,
}

Expand All @@ -134,7 +144,7 @@ impl DboAnalyzeContext {
}

/// Possible errors that might occur during analyzing.
#[derive(Debug, Eq, thiserror::Error, PartialEq, Serialize, TypescriptDefinition)]
#[derive(Debug, Eq, thiserror::Error, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum AnalyzeError {
#[error("Language construct unsupported: {0:?}")]
Expand Down Expand Up @@ -174,27 +184,18 @@ pub fn analyze(

/// WASM export of [`analyze()`]. Should _never_ be called from other Rust code.
///
/// A second, WASM-specific function is needed here. Since the only allowed
/// [`Result`] type to return to JS is a [`Result<T, JsValue>`], we just call
/// the actual [`analyze()`] function and map the error type.
/// A second, WASM-specific function is required here, as the only allowed [`Result`] type for
/// returning to JS is [`Result<T, JsValue>`]. We simply call the actual [`analyze()`] function and
/// map the error type.
///
/// For one, the main [`analyze()`] function shouldn't return a
/// [`JsValue`][`wasm_bindgen::JsValue`], since it should represent the "normal"
/// entry point into the library (e.g. from other Rust code). And secondly,
/// [`JsValue`][`wasm_bindgen::JsValue`] doesn't implement the
/// [`Debug`][`std::fmt::Debug`] trait, which just complicates unit tests.
/// And secondly, we want to transparently parse
/// [`DboAnalyzeContext`][`self::DboAnalyzeContext`] from the raw JS value
/// and pass it on.
/// The main [`analyze()`] function should not return a [`JsValue`][`wasm_bindgen::JsValue`],
/// since it represents the "normal" entry point into the library (e.g. from other Rust code).
/// Furthermore, [`JsValue`][`wasm_bindgen::JsValue`] does not implement the
/// [`Debug`][`std::fmt::Debug`] trait, making unit tests unnecessarily complex.
#[cfg(any(target_arch = "wasm32", target_arch = "wasm64"))]
#[wasm_bindgen(js_name = "analyze")]
pub fn js_analyze(typ: DboType, sql: &str, ctx: JsValue) -> Result<JsValue, JsValue> {
let ctx = serde_wasm_bindgen::from_value(ctx)?;

match analyze(typ, sql, &ctx) {
Ok(metadata) => Ok(serde_wasm_bindgen::to_value(&metadata)?),
Err(err) => Err(serde_wasm_bindgen::to_value(&err)?),
}
pub fn js_analyze(typ: DboType, sql: &str, ctx: DboAnalyzeContext) -> Result<DboMetaData, JsValue> {
analyze(typ, sql, &ctx).or_else(|err| Err(serde_wasm_bindgen::to_value(&err)?))
}

fn analyze_function(
Expand Down
12 changes: 6 additions & 6 deletions src/rules/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ use super::{check_parameter_types, replace_token, RuleDefinition, RuleError, Rul
pub(super) struct FixTrunc;

impl RuleDefinition for FixTrunc {
fn short_desc(&self) -> &'static str {
"Fix `trunc()` usage based on type"
fn short_desc(&self) -> String {
"Fix `trunc()` usage based on type".to_string()
}

fn find_rules(
Expand Down Expand Up @@ -50,8 +50,8 @@ impl RuleDefinition for FixTrunc {
pub(super) struct ReplaceSysdate;

impl RuleDefinition for ReplaceSysdate {
fn short_desc(&self) -> &'static str {
"Replace `SYSDATE` with PostgreSQL's `clock_timestamp()`"
fn short_desc(&self) -> String {
"Replace `SYSDATE` with PostgreSQL's `clock_timestamp()`".to_string()
}

fn find_rules(
Expand Down Expand Up @@ -80,8 +80,8 @@ impl RuleDefinition for ReplaceSysdate {
pub(super) struct ReplaceNvl;

impl RuleDefinition for ReplaceNvl {
fn short_desc(&self) -> &'static str {
"Replace `NVL` with PostgreSQL's `coalesce`"
fn short_desc(&self) -> String {
"Replace `NVL` with PostgreSQL's `coalesce`".to_string()
}

fn find_rules(
Expand Down
63 changes: 40 additions & 23 deletions src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ use std::ops::Range;
use indexmap::IndexMap;
use rowan::{Direction, GreenNode, GreenToken, NodeOrToken, TextRange, TokenAtOffset};
use serde::{Deserialize, Serialize};
use tsify::Tsify;
use unicode_width::UnicodeWidthStr;
use wasm_bindgen::prelude::*;
use wasm_typescript_definition::TypescriptDefinition;

use crate::analyze::{DboAnalyzeContext, DboType};
use crate::ast::{AstNode, Function, Param, Procedure, Root};
Expand Down Expand Up @@ -46,7 +46,7 @@ lazy_static::lazy_static! {
};
}

#[derive(Debug, Eq, thiserror::Error, PartialEq, Serialize, TypescriptDefinition)]
#[derive(Debug, Eq, thiserror::Error, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum RuleError {
#[error("Item not found: {0}")]
Expand All @@ -66,7 +66,7 @@ pub enum RuleError {
}

trait RuleDefinition {
fn short_desc(&self) -> &'static str;
fn short_desc(&self) -> String;
fn find_rules(&self, root: &Root, ctx: &DboAnalyzeContext)
-> Result<Vec<RuleMatch>, RuleError>;
fn apply(
Expand Down Expand Up @@ -95,14 +95,16 @@ impl RuleMatch {
}
}

#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize, TypescriptDefinition)]
#[derive(Tsify, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct RuleLocation {
offset: Range<u32>,
start: LineCol,
end: LineCol,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize, TypescriptDefinition)]
#[derive(Tsify, Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct LineCol {
line: u32,
col: u32,
Expand Down Expand Up @@ -140,11 +142,28 @@ impl fmt::Display for RuleLocation {
}
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize, TypescriptDefinition)]
#[derive(Tsify, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct RuleHint {
name: String,
locations: Vec<RuleLocation>,
short_desc: &'static str,
short_desc: String,
}

#[derive(Tsify, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct RuleApplication {
original: String,
locations: Vec<RuleLocation>,
}

impl RuleApplication {
fn new(original: String, locations: Vec<RuleLocation>) -> Self {
RuleApplication {
original,
locations,
}
}
}

impl From<ParseError> for RuleError {
Expand Down Expand Up @@ -178,7 +197,7 @@ pub fn apply_rule(
rule_name: &str,
location: Option<&RuleLocation>,
ctx: &DboAnalyzeContext,
) -> Result<(String, Vec<RuleLocation>), RuleError> {
) -> Result<RuleApplication, RuleError> {
let apply = |p: Parse| {
let rule = ANALYZER_RULES
.get(rule_name)
Expand All @@ -197,7 +216,10 @@ pub fn apply_rule(
let range = rule.apply(&node.unwrap().node, location, ctx)?;
let text = root.syntax().to_string();
let location = RuleLocation::from(&text, range);
Ok((root.syntax().to_string(), vec![location]))
Ok(RuleApplication::new(
root.syntax().to_string(),
vec![location],
))
} else {
let mut result = Vec::new();
while let Ok(locations) = rule.find_rules(&root, ctx) {
Expand All @@ -215,7 +237,7 @@ pub fn apply_rule(
.into_iter()
.map(|r| RuleLocation::from(&text, r))
.collect();
Ok((text, result))
Ok(RuleApplication::new(text, result))
}
};

Expand All @@ -233,16 +255,11 @@ pub fn js_apply_rule(
typ: DboType,
sql: &str,
rule: &str,
location: JsValue,
ctx: JsValue,
) -> Result<JsValue, JsValue> {
let location: Option<RuleLocation> = serde_wasm_bindgen::from_value(location)?;
let ctx = serde_wasm_bindgen::from_value(ctx)?;

match apply_rule(typ, sql, rule, location.as_ref(), &ctx) {
Ok(location) => Ok(serde_wasm_bindgen::to_value(&location)?),
Err(err) => Err(serde_wasm_bindgen::to_value(&err)?),
}
location: Option<RuleLocation>,
ctx: DboAnalyzeContext,
) -> Result<RuleApplication, JsValue> {
apply_rule(typ, sql, rule, location.as_ref(), &ctx)
.or_else(|err| Err(serde_wasm_bindgen::to_value(&err)?))
}

/// Finds all descendant nodes within an AST node that fulfill the predicate
Expand Down Expand Up @@ -431,7 +448,7 @@ mod tests {
&context,
);
assert!(result.is_ok(), "{:#?}", result);
transpiled = result.unwrap().0;
transpiled = result.unwrap().original;

let result = crate::analyze(DboType::Procedure, &transpiled, &context);
assert!(result.is_ok(), "{:#?}", result);
Expand Down Expand Up @@ -518,7 +535,7 @@ mod tests {
let mut do_apply = |rule: &RuleHint| {
let result = apply_rule(DboType::Procedure, &transpiled, &rule.name, None, &context);
assert!(result.is_ok(), "{:#?}", result);
transpiled = result.unwrap().0;
transpiled = result.unwrap().original;

let result = crate::analyze(DboType::Procedure, &transpiled, &context);
assert!(result.is_ok(), "{:#?}", result);
Expand Down Expand Up @@ -581,7 +598,7 @@ mod tests {
&context,
);
assert!(result.is_ok(), "{:#?}", result);
transpiled = result.unwrap().0;
transpiled = result.unwrap().original;

let result = crate::analyze(DboType::Procedure, &transpiled, &context);
assert!(result.is_ok(), "{:#?}", result);
Expand Down
Loading

0 comments on commit 75ed336

Please sign in to comment.