Skip to content

Commit

Permalink
Merge pull request #910 from demergent-labs/902_tighten_external_cani…
Browse files Browse the repository at this point in the history
…sters

Changed External Canister Parsing Errors
  • Loading branch information
lastmjs authored Feb 22, 2023
2 parents 84fe641 + 6b49c13 commit b33e15c
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#[derive(Clone, Debug)]
pub enum ParseError {
InvalidDecorator,
InvalidReturnType,
MissingCanisterResultAnnotation,
MissingDecorator,
MissingTypeAnnotation,
MissingTypeArgument,
MultipleDecorators,
NamespaceQualifiedType,
TooManyReturnTypes,
UnallowedComputedProperty,
}

impl ParseError {
pub fn error_message(&self) -> String {
let str = match self {
Self::InvalidDecorator => "Invalid decorator. Only @query and @update are permitted.",
Self::InvalidReturnType => "Method has an invalid return type. Only function return types are permitted.",
Self::MissingCanisterResultAnnotation => "Invalid return type. External canister methods must wrap their return types in the CanisterResult<T> generic type.",
Self::MissingDecorator => "Missing decorator. External canister methods must be decorated with either @query or @update.",
Self::MissingTypeAnnotation => "Missing type annotation. External canister methods must specify a return type.",
Self::MissingTypeArgument => "Missing type argument. Generic type CanisterResult requires 1 type argument.",
Self::MultipleDecorators => "Too many decorators. External canister methods can only specify one decorator: @query or @update.",
Self::NamespaceQualifiedType => "Unsupported data type. Qualified types are not currently supported. Try importing the type directly.",
Self::TooManyReturnTypes => "Too many return types. Generic type CanisterResult requires 1 type argument.",
Self::UnallowedComputedProperty => "Unallowed computed property. Computed properties in external canister definitions aren't currently supported.",
};
str.to_string()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use annotate_snippets::{
};
use std::fmt;

pub mod external_canister_method;

pub struct Suggestion {
pub title: String,
pub source: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use swc_ecma_ast::{ClassDecl, ClassMember, ClassProp};

use crate::{
errors::external_canister_method::ParseError,
ts_ast::{
source_map::{GetSourceFileInfo, SourceMapped},
GetName,
},
};

impl SourceMapped<'_, ClassDecl> {
pub fn build_invalid_class_member_error_message(&self, class_member: &ClassMember) -> String {
let member_type = match class_member {
ClassMember::Constructor(_) => "constructor",
ClassMember::Method(_) => "method",
ClassMember::PrivateMethod(_) => "private method",
ClassMember::ClassProp(_) => "class prop",
ClassMember::PrivateProp(_) => "private prop",
ClassMember::TsIndexSignature(_) => "TS index signature",
ClassMember::Empty(_) => "empty block",
ClassMember::StaticBlock(_) => "static block",
};

let span = match class_member {
ClassMember::Constructor(constructor) => constructor.span,
ClassMember::Method(method) => method.span,
ClassMember::PrivateMethod(private_method) => private_method.span,
ClassMember::ClassProp(class_prop) => class_prop.span,
ClassMember::PrivateProp(private_prop) => private_prop.span,
ClassMember::TsIndexSignature(ts_index_signature) => ts_index_signature.span,
ClassMember::Empty(empty) => empty.span,
ClassMember::StaticBlock(static_block) => static_block.span,
};

let external_canister_class_name = self.ident.get_name().to_string();

let origin = self.source_map.get_origin(span);
let line_number = self.source_map.get_line_number(span);
let column_number = self.source_map.get_range(span).0 + 1;

format!(
"Invalid {} in class {}\nat {}:{}:{}\n\nHelp: Remove this member or make it a property",
member_type, external_canister_class_name, origin, line_number, column_number
)
}

pub fn build_invalid_class_prop_error_message(
&self,
class_prop: &ClassProp,
error_message: ParseError,
) -> String {
let external_canister_class_name = self.ident.get_name().to_string();

let origin = self.source_map.get_origin(class_prop.span);
let line_number = self.source_map.get_line_number(class_prop.span);
let column_number = self.source_map.get_range(class_prop.span).0 + 1;
let location = format!("{}:{}:{}", origin, line_number, column_number);

format!(
"{}\n\nin class {}\nat {}",
error_message.error_message(),
external_canister_class_name,
location
)
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
mod errors;
mod get_dependencies;
mod to_act_external_canister;
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,31 @@ impl SourceMapped<'_, ClassDecl> {
}

fn build_external_canister_methods(&self) -> Vec<ActExternalCanisterMethod> {
if self.class.body.len() == 0 {
panic!("external canister declarations must contain a constructor and at least one property")
}

self.class
.body
.iter()
.fold(vec![], |mut acc, class_member| {
if let ClassMember::ClassProp(class_prop) = class_member {
.fold(vec![], |mut acc, class_member| match class_member {
ClassMember::ClassProp(class_prop) => {
let class_prop_with_source_map = SourceMapped::new(class_prop, self.source_map);
let possible_canister_method =

let canister_method_result =
class_prop_with_source_map.to_act_external_canister_method();
if let Some(canister_method) = possible_canister_method {
acc.push(canister_method);

match canister_method_result {
Ok(canister_method) => {
acc.push(canister_method);
acc
}
Err(e) => panic!(
"{}",
self.build_invalid_class_prop_error_message(class_prop, e)
),
}
}
// TODO: Handle other types of class members. Decide if we should
// Error out, or give a warning, or what.
acc
_ => panic!(
"{}",
self.build_invalid_class_member_error_message(class_member)
),
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ impl GetDependencies for SourceMapped<'_, ClassProp> {
type_alias_lookup: &std::collections::HashMap<String, crate::ts_ast::AzleTypeAliasDecl>,
found_type_names: &std::collections::HashSet<String>,
) -> std::collections::HashSet<String> {
let return_types = self.return_ts_type();
let param_types = self.param_ts_types();
let return_types = self.return_ts_type().unwrap(); // Considering unwrap safe because errors should have been caught when parsing the nodes, not the dependencies
let param_types = self.param_ts_types().unwrap(); // Considering unwrap safe because errors should have been caught when parsing the nodes, not the dependencies
let ts_types = vec![vec![return_types], param_types].concat();

ts_types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,46 @@ use cdk_framework::{
};
use swc_ecma_ast::{ClassProp, Expr, TsFnOrConstructorType, TsFnType, TsType};

use crate::ts_ast::{source_map::SourceMapped, GetName};
use crate::{
errors::external_canister_method::ParseError,
ts_ast::{source_map::SourceMapped, GetName},
};

use super::azle_type::AzleType;

mod get_dependent_types;

impl SourceMapped<'_, ClassProp> {
pub fn to_act_external_canister_method(&self) -> Option<ActExternalCanisterMethod> {
pub fn to_act_external_canister_method(&self) -> Result<ActExternalCanisterMethod, ParseError> {
if self.decorators.len() == 0 {
return Err(ParseError::MissingDecorator);
}

if !self.has_azle_decorator() {
return None;
return Err(ParseError::InvalidDecorator);
}

let name = self.name();
let params = self.build_act_fn_params();
let return_type = self.build_return_type();
let name = self.name()?;
let _mode = self.mode()?;
let params = self.build_act_fn_params()?;
let return_type = self.build_return_type()?;

Some(ActExternalCanisterMethod {
Ok(ActExternalCanisterMethod {
name,
params,
return_type,
})
}

fn build_act_fn_params(&self) -> Vec<ActFnParam> {
self.ts_fn_type().build_act_fn_params()
fn build_act_fn_params(&self) -> Result<Vec<ActFnParam>, ParseError> {
Ok(self.ts_fn_type()?.build_act_fn_params())
}

fn build_return_type(&self) -> ActDataType {
let return_ts_type = self.return_ts_type();
fn build_return_type(&self) -> Result<ActDataType, ParseError> {
let return_ts_type = self.return_ts_type()?;
let azle_type = AzleType::from_ts_type(return_ts_type, self.source_map);
azle_type.to_act_data_type(&None)
let act_data_type = azle_type.to_act_data_type(&None);
Ok(act_data_type)
}

fn contains_decorator(&self, name: &str) -> bool {
Expand All @@ -50,80 +59,89 @@ impl SourceMapped<'_, ClassProp> {
self.contains_decorator("query") || self.contains_decorator("update")
}

fn mode(&self) -> String {
fn mode(&self) -> Result<String, ParseError> {
if self.decorators.len() != 1 {
panic!(
"InvalidExternalCanisterDeclaration: child property specifies multiple decorators."
);
return Err(ParseError::MultipleDecorators);
};

self.decorators
let mode = self
.decorators
.get(0)
.unwrap()
.expr
.as_ident()
.unwrap()
.get_name()
.to_string()
.to_string();

Ok(mode)
}

fn name(&self) -> String {
match &self.key {
fn name(&self) -> Result<String, ParseError> {
let name = match &self.key {
swc_ecma_ast::PropName::Ident(ident) => ident.get_name().to_string(),
swc_ecma_ast::PropName::Str(str) => str.value.to_string(),
swc_ecma_ast::PropName::Num(num) => num.value.to_string(),
swc_ecma_ast::PropName::Computed(_) => panic!("InvalidExternalCanisterDeclaration: contains a computed method name which isn't currently supported"),
swc_ecma_ast::PropName::Computed(_) => {
return Err(ParseError::UnallowedComputedProperty)
}
swc_ecma_ast::PropName::BigInt(big_int) => big_int.value.to_string(),
}
};

return Ok(name);
}

fn param_ts_types(&self) -> Vec<TsType> {
self.ts_fn_type().get_param_ts_types()
fn param_ts_types(&self) -> Result<Vec<TsType>, ParseError> {
Ok(self.ts_fn_type()?.get_param_ts_types())
}

fn return_ts_type(&self) -> TsType {
let ts_fn_type = self.ts_fn_type();
fn return_ts_type(&self) -> Result<TsType, ParseError> {
let ts_fn_type = self.ts_fn_type()?;
match &*ts_fn_type.type_ann.type_ann {
TsType::TsTypeRef(ts_type_ref) => {
let name = match &ts_type_ref.type_name {
swc_ecma_ast::TsEntityName::TsQualifiedName(_) => panic!("InvalidExternalCanisterDeclaration: qualified names in member return types are not currently supported. Try importing the type directly."),
swc_ecma_ast::TsEntityName::TsQualifiedName(_) => {
return Err(ParseError::NamespaceQualifiedType)
}
swc_ecma_ast::TsEntityName::Ident(ident) => ident.get_name().to_string(),
};

if name != "CanisterResult" {
panic!("InvalidExternalCanisterDeclaration: return type of property \"{}\" is not a CanisterResult. External canister methods must wrap their return types in the CanisterResult<T> generic type.", self.name())
return Err(ParseError::MissingCanisterResultAnnotation);
}

match &ts_type_ref.type_params {
Some(ts_type_param_inst) => {
if ts_type_param_inst.params.len() != 1 {
panic!("InvalidExternalCanisterDeclaration: incorrect number of type arguments to generic type CanisterResult<T> for property \"{}\".", self.name())
return Err(ParseError::TooManyReturnTypes);
}

let inner_type = &**ts_type_param_inst.params.get(0).unwrap();
inner_type.clone()
},
None => panic!("InvalidExternalCanisterDeclaration: missing type argument to generic return type CanisterResult<T> for property \"{}\".", self.name())
Ok(inner_type.clone())
}
None => return Err(ParseError::MissingTypeArgument),
}
},
_ => panic!("InvalidExternalCanisterDeclaration: return type of property \"{}\" is not a CanisterResult. External canister methods must wrap their return types in the CanisterResult<T> generic type.", self.name())
}
_ => return Err(ParseError::MissingCanisterResultAnnotation),
}
}

fn ts_fn_type(&self) -> SourceMapped<TsFnType> {
fn ts_fn_type(&self) -> Result<SourceMapped<TsFnType>, ParseError> {
match &self.type_ann {
Some(type_ann) => match &*type_ann.type_ann {
TsType::TsFnOrConstructorType(fn_or_constructor_type) => {
match fn_or_constructor_type {
TsFnOrConstructorType::TsFnType(ts_fn_type) => SourceMapped::new(ts_fn_type, self.source_map),
TsFnOrConstructorType::TsConstructorType(_) => panic!("InvalidExternalCanisterDeclaration: Decorator \"@{}\" not allowed on constructor", self.mode()),
TsFnOrConstructorType::TsFnType(ts_fn_type) => {
Ok(SourceMapped::new(ts_fn_type, self.source_map))
}
TsFnOrConstructorType::TsConstructorType(_) => {
return Err(ParseError::InvalidReturnType)
}
}
}
_ => panic!(
"InvalidExternalCanisterDeclaration: method \"{}\" has an invalid return type. Only function return types are permitted", self.name()
),
_ => return Err(ParseError::InvalidReturnType),
},
None => panic!("InvalidExternalCanisterDeclaration: method \"{}\" is missing a type annotation", self.name()),
None => return Err(ParseError::MissingTypeAnnotation),
}
}
}

0 comments on commit b33e15c

Please sign in to comment.