From d067633f335e4cc5273d42d3b1730ebed17b1f4d Mon Sep 17 00:00:00 2001 From: Christoph Burgdorf Date: Thu, 16 Jun 2022 11:16:47 +0200 Subject: [PATCH] Treat `trait` exclusively as `Item` not `Type` --- crates/analyzer/src/db/queries/contracts.rs | 8 -- crates/analyzer/src/db/queries/functions.rs | 22 ++---- crates/analyzer/src/db/queries/module.rs | 12 ++- crates/analyzer/src/namespace/items.rs | 24 +++--- crates/analyzer/src/namespace/types.rs | 5 +- crates/analyzer/src/operations.rs | 1 - crates/analyzer/src/traversal/expressions.rs | 40 +++++++--- crates/analyzer/src/traversal/types.rs | 74 ++++++++++++++++++- crates/analyzer/tests/analysis.rs | 2 +- .../snapshots/errors__traits_as_fields.snap | 18 +++-- crates/mir/src/lower/types.rs | 3 - crates/parser/src/grammar/functions.rs | 2 +- crates/parser/tests/cases/errors.rs | 2 + .../cases__errors__fn_invalid_bound.snap | 12 +++ 14 files changed, 151 insertions(+), 74 deletions(-) create mode 100644 crates/parser/tests/cases/snapshots/cases__errors__fn_invalid_bound.snap diff --git a/crates/analyzer/src/db/queries/contracts.rs b/crates/analyzer/src/db/queries/contracts.rs index e97b086a51..4b736c488a 100644 --- a/crates/analyzer/src/db/queries/contracts.rs +++ b/crates/analyzer/src/db/queries/contracts.rs @@ -330,14 +330,6 @@ pub fn contract_field_type( let node = &field.data(db).ast; - if let Ok(Type::Trait(ref val)) = typ { - scope.error( - "traits can not be used as contract fields", - node.span, - &format!("trait `{}` can not appear here", val.name), - ); - } - if node.kind.is_pub { scope.not_yet_implemented("contract `pub` fields", node.span); } diff --git a/crates/analyzer/src/db/queries/functions.rs b/crates/analyzer/src/db/queries/functions.rs index 2aab4dc6b9..e5c13afebd 100644 --- a/crates/analyzer/src/db/queries/functions.rs +++ b/crates/analyzer/src/db/queries/functions.rs @@ -7,7 +7,7 @@ use crate::namespace::items::{ use crate::namespace::scopes::{BlockScope, BlockScopeType, FunctionScope, ItemScope}; use crate::namespace::types::{self, Contract, CtxDecl, Generic, SelfDecl, Struct, Type}; use crate::traversal::functions::traverse_statements; -use crate::traversal::types::type_desc; +use crate::traversal::types::{type_desc, type_desc_to_trait}; use fe_common::diagnostics::Label; use fe_parser::ast::{self, GenericParameter}; use fe_parser::node::Node; @@ -255,17 +255,9 @@ pub fn resolve_function_param_type( if let Some(val) = function.generic_param(db, base) { let bounds = match val { ast::GenericParameter::Unbounded(_) => vec![], - ast::GenericParameter::Bounded { bound, .. } => match type_desc(context, &bound)? { - Type::Trait(trait_ty) => vec![trait_ty.id], - other => { - context.error( - &format!("expected trait, found type `{}`", other), - bound.span, - "not a trait", - ); - vec![] - } - }, + ast::GenericParameter::Bounded { bound, .. } => { + vec![type_desc_to_trait(context, &bound)?] + } }; return Ok(Type::Generic(Generic { @@ -401,11 +393,7 @@ pub fn function_dependency_graph(db: &dyn AnalyzerDb, function: FunctionId) -> D directs.push((root, Item::Function(*method), DepLocality::Local)); } CallType::TraitValueMethod { trait_id, .. } => { - directs.push(( - root, - Item::Type(TypeDef::Trait(*trait_id)), - DepLocality::Local, - )); + directs.push((root, Item::Trait(*trait_id), DepLocality::Local)); } CallType::External { contract, function } => { directs.push((root, Item::Function(*function), DepLocality::External)); diff --git a/crates/analyzer/src/db/queries/module.rs b/crates/analyzer/src/db/queries/module.rs index 380c0af16b..1636c2e824 100644 --- a/crates/analyzer/src/db/queries/module.rs +++ b/crates/analyzer/src/db/queries/module.rs @@ -89,12 +89,10 @@ pub fn module_all_items(db: &dyn AnalyzerDb, module: ModuleId) -> Rc<[Item]> { ast::ModuleStmt::Function(node) => Some(Item::Function( db.intern_function(Rc::new(Function::new(db, node, None, module))), )), - ast::ModuleStmt::Trait(node) => Some(Item::Type(TypeDef::Trait(db.intern_trait( - Rc::new(Trait { - ast: node.clone(), - module, - }), - )))), + ast::ModuleStmt::Trait(node) => Some(Item::Trait(db.intern_trait(Rc::new(Trait { + ast: node.clone(), + module, + })))), ast::ModuleStmt::Pragma(_) | ast::ModuleStmt::Use(_) | ast::ModuleStmt::Impl(_) => None, ast::ModuleStmt::Event(node) => Some(Item::Event(db.intern_event(Rc::new(Event { ast: node.clone(), @@ -119,7 +117,7 @@ pub fn module_all_impls(db: &dyn AnalyzerDb, module: ModuleId) -> Rc<[ImplId]> { let mut scope = ItemScope::new(db, module); let receiver_type = type_desc(&mut scope, &impl_node.kind.receiver).unwrap(); - if let Some(Item::Type(TypeDef::Trait(val))) = treit { + if let Some(Item::Trait(val)) = treit { Some(db.intern_impl(Rc::new(Impl { trait_id: val, receiver: receiver_type, diff --git a/crates/analyzer/src/namespace/items.rs b/crates/analyzer/src/namespace/items.rs index b5421f4faa..372538810c 100644 --- a/crates/analyzer/src/namespace/items.rs +++ b/crates/analyzer/src/namespace/items.rs @@ -36,6 +36,7 @@ pub enum Item { // Events aren't normal types; they *could* be moved into // TypeDef, but it would have consequences. Event(EventId), + Trait(TraitId), Function(FunctionId), Constant(ModuleConstantId), // Needed until we can represent keccak256 as a FunctionId. @@ -48,6 +49,7 @@ impl Item { pub fn name(&self, db: &dyn AnalyzerDb) -> SmolStr { match self { Item::Type(id) => id.name(db), + Item::Trait(id) => id.name(db), Item::GenericType(id) => id.name(), Item::Event(id) => id.name(db), Item::Function(id) => id.name(db), @@ -62,6 +64,7 @@ impl Item { pub fn name_span(&self, db: &dyn AnalyzerDb) -> Option { match self { Item::Type(id) => id.name_span(db), + Item::Trait(id) => Some(id.name_span(db)), Item::GenericType(_) => None, Item::Event(id) => Some(id.name_span(db)), Item::Function(id) => Some(id.name_span(db)), @@ -81,6 +84,7 @@ impl Item { | Self::Intrinsic(_) | Self::GenericType(_) => true, Self::Type(id) => id.is_public(db), + Self::Trait(id) => id.is_public(db), Self::Event(id) => id.is_public(db), Self::Function(id) => id.is_public(db), Self::Constant(id) => id.is_public(db), @@ -94,6 +98,7 @@ impl Item { | Item::BuiltinFunction(_) | Item::Intrinsic(_) => true, Item::Type(_) + | Item::Trait(_) | Item::Event(_) | Item::Function(_) | Item::Constant(_) @@ -113,6 +118,7 @@ impl Item { pub fn item_kind_display_name(&self) -> &'static str { match self { Item::Type(_) | Item::GenericType(_) => "type", + Item::Trait(_) => "trait", Item::Event(_) => "event", Item::Function(_) | Item::BuiltinFunction(_) => "function", Item::Intrinsic(_) => "intrinsic function", @@ -129,6 +135,7 @@ impl Item { Item::Type(val) => val.items(db), Item::GenericType(_) | Item::Event(_) + | Item::Trait(_) | Item::Function(_) | Item::Constant(_) | Item::BuiltinFunction(_) @@ -139,6 +146,7 @@ impl Item { pub fn parent(&self, db: &dyn AnalyzerDb) -> Option { match self { Item::Type(id) => id.parent(db), + Item::Trait(id) => Some(id.parent(db)), Item::GenericType(_) => None, Item::Event(id) => Some(id.parent(db)), Item::Function(id) => Some(id.parent(db)), @@ -222,6 +230,7 @@ impl Item { pub fn sink_diagnostics(&self, db: &dyn AnalyzerDb, sink: &mut impl DiagnosticSink) { match self { Item::Type(id) => id.sink_diagnostics(db, sink), + Item::Trait(id) => id.sink_diagnostics(db, sink), Item::Event(id) => id.sink_diagnostics(db, sink), Item::Function(id) => id.sink_diagnostics(db, sink), Item::GenericType(_) | Item::BuiltinFunction(_) | Item::Intrinsic(_) => {} @@ -772,7 +781,6 @@ impl ModuleConstantId { pub enum TypeDef { Alias(TypeAliasId), Struct(StructId), - Trait(TraitId), Contract(ContractId), Primitive(types::Base), } @@ -804,7 +812,6 @@ impl TypeDef { match self { TypeDef::Alias(id) => id.name(db), TypeDef::Struct(id) => id.name(db), - TypeDef::Trait(id) => id.name(db), TypeDef::Contract(id) => id.name(db), TypeDef::Primitive(typ) => typ.name(), } @@ -814,7 +821,6 @@ impl TypeDef { match self { TypeDef::Alias(id) => Some(id.name_span(db)), TypeDef::Struct(id) => Some(id.name_span(db)), - TypeDef::Trait(id) => Some(id.name_span(db)), TypeDef::Contract(id) => Some(id.name_span(db)), TypeDef::Primitive(_) => None, } @@ -828,10 +834,6 @@ impl TypeDef { name: id.name(db), field_count: id.fields(db).len(), // for the EvmSized trait })), - TypeDef::Trait(id) => Ok(types::Type::Trait(types::Trait { - id: *id, - name: id.name(db), - })), TypeDef::Contract(id) => Ok(types::Type::Contract(types::Contract { id: *id, name: id.name(db), @@ -844,7 +846,6 @@ impl TypeDef { match self { Self::Alias(id) => id.is_public(db), Self::Struct(id) => id.is_public(db), - Self::Trait(id) => id.is_public(db), Self::Contract(id) => id.is_public(db), Self::Primitive(_) => true, } @@ -854,7 +855,6 @@ impl TypeDef { match self { TypeDef::Alias(id) => Some(id.parent(db)), TypeDef::Struct(id) => Some(id.parent(db)), - TypeDef::Trait(id) => Some(id.parent(db)), TypeDef::Contract(id) => Some(id.parent(db)), TypeDef::Primitive(_) => None, } @@ -864,7 +864,6 @@ impl TypeDef { match self { TypeDef::Alias(id) => id.sink_diagnostics(db, sink), TypeDef::Struct(id) => id.sink_diagnostics(db, sink), - TypeDef::Trait(id) => id.sink_diagnostics(db, sink), TypeDef::Contract(id) => id.sink_diagnostics(db, sink), TypeDef::Primitive(_) => {} } @@ -1158,7 +1157,7 @@ impl FunctionSigId { } pub fn is_trait_fn(&self, db: &dyn AnalyzerDb) -> bool { - matches!(self.parent(db), Item::Type(TypeDef::Trait(_))) + matches!(self.parent(db), Item::Trait(_)) } pub fn is_impl_fn(&self, db: &dyn AnalyzerDb) -> bool { @@ -1344,7 +1343,7 @@ impl Class { match self { Class::Contract(id) => Item::Type(TypeDef::Contract(*id)), Class::Struct(id) => Item::Type(TypeDef::Struct(*id)), - Class::Trait(id) => Item::Type(TypeDef::Trait(*id)), + Class::Trait(id) => Item::Trait(*id), // Coercing into an Item of the basis of the receiver doesn't seem ideal but can hopefully // be addressed when we get rid of `Class`. Class::Impl(id) => id @@ -1595,7 +1594,6 @@ impl ImplId { Type::Contract(_) | Type::Map(_) | Type::SelfContract(_) - | Type::Trait(_) | Type::Generic(_) // TODO: We should find a way to support these types. We only support implementing traits for structs // so far because it simplifies things regarding the assign location of the underlying type. diff --git a/crates/analyzer/src/namespace/types.rs b/crates/analyzer/src/namespace/types.rs index 0d9a70b51a..4bf4ebe447 100644 --- a/crates/analyzer/src/namespace/types.rs +++ b/crates/analyzer/src/namespace/types.rs @@ -47,7 +47,6 @@ pub enum Type { /// of `self` within a contract function. SelfContract(Contract), Struct(Struct), - Trait(Trait), Generic(Generic), } @@ -410,7 +409,6 @@ impl Type { Type::Tuple(inner) => inner.to_string().into(), Type::String(inner) => inner.to_string().into(), Type::Struct(inner) => inner.name.clone(), - Type::Trait(inner) => inner.name.clone(), Type::Generic(inner) => inner.name.clone(), Type::Contract(inner) | Type::SelfContract(inner) => inner.name.clone(), } @@ -482,7 +480,7 @@ impl Type { | Type::Struct(_) | Type::Generic(_) | Type::Contract(_) => true, - Type::Map(_) | Type::SelfContract(_) | Type::Trait(_) => false, + Type::Map(_) | Type::SelfContract(_) => false, } } } @@ -686,7 +684,6 @@ impl fmt::Display for Type { Type::Contract(inner) => inner.fmt(f), Type::SelfContract(inner) => inner.fmt(f), Type::Struct(inner) => inner.fmt(f), - Type::Trait(inner) => inner.fmt(f), Type::Generic(inner) => inner.fmt(f), } } diff --git a/crates/analyzer/src/operations.rs b/crates/analyzer/src/operations.rs index b8251ee90d..b6d0e883eb 100644 --- a/crates/analyzer/src/operations.rs +++ b/crates/analyzer/src/operations.rs @@ -15,7 +15,6 @@ pub fn index(value: Type, index: Type) -> Result { | Type::String(_) | Type::Contract(_) | Type::SelfContract(_) - | Type::Trait(_) | Type::Generic(_) | Type::Struct(_) => Err(IndexingError::NotSubscriptable), } diff --git a/crates/analyzer/src/traversal/expressions.rs b/crates/analyzer/src/traversal/expressions.rs index 6f2783a312..79c319cbdf 100644 --- a/crates/analyzer/src/traversal/expressions.rs +++ b/crates/analyzer/src/traversal/expressions.rs @@ -3,7 +3,6 @@ use crate::context::{AnalyzerContext, CallType, ExpressionAttributes, Location, use crate::errors::{FatalError, IndexingError}; use crate::namespace::items::{Class, FunctionId, Item, TypeDef}; use crate::namespace::scopes::BlockScopeType; -use crate::namespace::types::Trait; use crate::namespace::types::{ Array, Base, Contract, FeString, Integer, Struct, Tuple, Type, TypeDowncast, U256, }; @@ -179,7 +178,7 @@ pub fn assignable_expr( attributes.move_location = Some(Location::Value); } } - Array(_) | Tuple(_) | String(_) | Struct(_) | Trait(_) | Generic(_) => { + Array(_) | Tuple(_) | String(_) | Struct(_) | Generic(_) => { if attributes.final_location() != Location::Memory { context.fancy_error( "value must be copied to memory", @@ -318,10 +317,24 @@ fn expr_named_thing( id.receiver(context.db()), Location::Memory, )), - Class::Trait(id) => Ok(ExpressionAttributes::new( - Type::Trait(Trait::from_id(id, context.db())), - Location::Memory, - )), + // This can only happen when trait methods can implement a default body + Class::Trait(id) => Err(FatalError::new(context.fancy_error( + &format!( + "`{}` is a trait, and can't be used as an expression", + exp.kind + ), + vec![ + Label::primary( + id.name_span(context.db()), + &format!("`{}` is defined here as a trait", exp.kind), + ), + Label::primary( + exp.span, + &format!("`{}` is used here as a value", exp.kind), + ), + ], + vec![], + ))), Class::Contract(id) => Ok(ExpressionAttributes::new( Type::SelfContract(Contract::from_id(id, context.db())), Location::Value, @@ -974,6 +987,14 @@ fn expr_call_named_thing( func.kind )], ))), + NamedThing::Item(Item::Trait(_)) => Err(FatalError::new(context.error( + &format!("`{}` is not callable", func.kind), + func.span, + &format!( + "`{}` is a trait, and can't be used as a function", + func.kind + ), + ))), NamedThing::Item(Item::Ingot(_)) => Err(FatalError::new(context.error( &format!("`{}` is not callable", func.kind), func.span, @@ -1129,11 +1150,7 @@ fn expr_call_type_constructor( Type::Struct(struct_type) => { return expr_call_struct_constructor(context, name_span, struct_type, args) } - Type::Base(Base::Bool) - | Type::Array(_) - | Type::Map(_) - | Type::Generic(_) - | Type::Trait(_) => { + Type::Base(Base::Bool) | Type::Array(_) | Type::Map(_) | Type::Generic(_) => { return Err(FatalError::new(context.error( &format!("`{}` type is not callable", typ.name()), name_span, @@ -1220,7 +1237,6 @@ fn expr_call_type_constructor( Type::Struct(_) => unreachable!(), // handled above Type::Map(_) => unreachable!(), // handled above Type::Array(_) => unreachable!(), // handled above - Type::Trait(_) => unreachable!(), // handled above Type::Generic(_) => unreachable!(), // handled above Type::SelfContract(_) => unreachable!(), /* unnameable; contract names all become * Type::Contract */ diff --git a/crates/analyzer/src/traversal/types.rs b/crates/analyzer/src/traversal/types.rs index 9ad755df28..e1a21e3f60 100644 --- a/crates/analyzer/src/traversal/types.rs +++ b/crates/analyzer/src/traversal/types.rs @@ -1,6 +1,6 @@ use crate::context::{AnalyzerContext, Constant, NamedThing}; use crate::errors::TypeError; -use crate::namespace::items::Item; +use crate::namespace::items::{Item, TraitId}; use crate::namespace::types::{GenericArg, GenericParamKind, GenericType, Tuple, Type}; use crate::traversal::call_args::validate_arg_count; use fe_common::diagnostics::Label; @@ -239,3 +239,75 @@ pub fn type_desc( ast::TypeDesc::Unit => Ok(Type::unit()), } } + +/// Maps a type description node to a `TraitId`. +pub fn type_desc_to_trait( + context: &mut dyn AnalyzerContext, + desc: &Node, +) -> Result { + match &desc.kind { + ast::TypeDesc::Base { base } => { + let named_thing = context.resolve_name(base, desc.span)?; + resolve_concrete_trait_named_thing(context, named_thing, desc) + } + ast::TypeDesc::Path(path) => { + let named_thing = context.resolve_path(path, desc.span); + resolve_concrete_trait_named_thing(context, named_thing, desc) + } + // generic will need to allow for paths too + ast::TypeDesc::Generic { base, .. } => { + let named_thing = context.resolve_name(&base.kind, desc.span)?; + resolve_concrete_trait_named_thing(context, named_thing, desc) + } + _ => panic!("Should be rejected by parser"), + } +} + +pub fn resolve_concrete_trait_named_thing( + context: &mut dyn AnalyzerContext, + val: Option, + base_desc: &Node, +) -> Result { + match val { + Some(NamedThing::Item(Item::Trait(treit))) => Ok(treit), + Some(NamedThing::Item(Item::Type(ty))) => Err(TypeError::new(context.error( + &format!("expected trait, found type `{}`", ty.name(context.db())), + base_desc.span, + "not a trait", + ))), + Some(named_thing) => Err(TypeError::new(context.fancy_error( + &format!("`{}` is not a trait name", base_desc.kind), + if let Some(def_span) = named_thing.name_span(context.db()) { + vec![ + Label::primary( + def_span, + format!( + "`{}` is defined here as a {}", + base_desc.kind, + named_thing.item_kind_display_name() + ), + ), + Label::primary( + base_desc.span, + format!("`{}` is used here as a trait", base_desc.kind), + ), + ] + } else { + vec![Label::primary( + base_desc.span, + format!( + "`{}` is a {}", + base_desc.kind, + named_thing.item_kind_display_name() + ), + )] + }, + vec![], + ))), + None => Err(TypeError::new(context.error( + "undefined trait", + base_desc.span, + &format!("`{}` has not been defined", base_desc.kind), + ))), + } +} diff --git a/crates/analyzer/tests/analysis.rs b/crates/analyzer/tests/analysis.rs index 2fcf9772dc..d1b4e58d66 100644 --- a/crates/analyzer/tests/analysis.rs +++ b/crates/analyzer/tests/analysis.rs @@ -345,7 +345,7 @@ fn build_snapshot(db: &dyn AnalyzerDb, module: items::ModuleId) -> String { .collect(), ] .concat(), - Item::Type(TypeDef::Trait(val)) => val + Item::Trait(val) => val .all_functions(db) .iter() .flat_map(|fun| { diff --git a/crates/analyzer/tests/snapshots/errors__traits_as_fields.snap b/crates/analyzer/tests/snapshots/errors__traits_as_fields.snap index 2f6755a385..98e583a0a4 100644 --- a/crates/analyzer/tests/snapshots/errors__traits_as_fields.snap +++ b/crates/analyzer/tests/snapshots/errors__traits_as_fields.snap @@ -3,16 +3,22 @@ source: crates/analyzer/tests/errors.rs expression: "error_string(&path, test_files::fixture(path))" --- -error: struct field type must have a fixed size - ┌─ compile_errors/traits_as_fields.fe:4:5 +error: `Foo` is not a type name + ┌─ compile_errors/traits_as_fields.fe:1:7 │ +1 │ trait Foo {} + │ ^^^ `Foo` is defined here as a trait + · 4 │ val: Foo - │ ^^^^^^^^ this can't be used as an struct field + │ ^^^ `Foo` is used here as a type -error: traits can not be used as contract fields - ┌─ compile_errors/traits_as_fields.fe:8:5 +error: `Foo` is not a type name + ┌─ compile_errors/traits_as_fields.fe:1:7 │ +1 │ trait Foo {} + │ ^^^ `Foo` is defined here as a trait + · 8 │ val: Foo - │ ^^^^^^^^ trait `Foo` can not appear here + │ ^^^ `Foo` is used here as a type diff --git a/crates/mir/src/lower/types.rs b/crates/mir/src/lower/types.rs index db0f611f4c..75135320c7 100644 --- a/crates/mir/src/lower/types.rs +++ b/crates/mir/src/lower/types.rs @@ -18,9 +18,6 @@ pub fn lower_type(db: &dyn MirDb, analyzer_ty: &analyzer_types::Type) -> TypeId analyzer_types::Type::Contract(_) => TypeKind::Address, analyzer_types::Type::SelfContract(contract) => lower_contract(db, contract), analyzer_types::Type::Struct(struct_) => lower_struct(db, struct_), - analyzer_types::Type::Trait(_) => { - panic!("traits should only appear in generic bounds for now") - } analyzer_types::Type::Generic(_) => { panic!("should be lowered in `lower_types_in_functions`") } diff --git a/crates/parser/src/grammar/functions.rs b/crates/parser/src/grammar/functions.rs index 4f55e5fc3d..59cd194ad1 100644 --- a/crates/parser/src/grammar/functions.rs +++ b/crates/parser/src/grammar/functions.rs @@ -128,7 +128,7 @@ pub fn parse_generic_param(par: &mut Parser) -> ParseResult { let name = par.assert(Name); match par.optional(Colon) { Some(_) => { - let bound = par.assert(Name); + let bound = par.expect(TokenKind::Name, "failed to parse generic bound")?; Ok(GenericParameter::Bounded { name: Node::new(name.text.into(), name.span), bound: Node::new( diff --git a/crates/parser/tests/cases/errors.rs b/crates/parser/tests/cases/errors.rs index e2f48f173e..ebeb74abdb 100644 --- a/crates/parser/tests/cases/errors.rs +++ b/crates/parser/tests/cases/errors.rs @@ -83,6 +83,8 @@ test_parse_err! { for_no_in, functions::parse_stmt, "for x {}" } test_parse_err! { fn_no_args, module::parse_module, "fn f {\n return 5\n}" } test_parse_err! { fn_unsafe_pub, module::parse_module, "unsafe pub fn f() {\n return 5 }" } test_parse_err! { fn_def_kw, module::parse_module, "contract C {\n pub def f(x: u8){\n return x \n}\n}" } + +test_parse_err! { fn_invalid_bound, module::parse_module, "pub fn f() {}" } test_parse_err! { use_bad_name, module::parse_use, "use x as 123" } test_parse_err! { module_bad_stmt, module::parse_module, "if x { y }" } test_parse_err! { module_nonsense, module::parse_module, "))" } diff --git a/crates/parser/tests/cases/snapshots/cases__errors__fn_invalid_bound.snap b/crates/parser/tests/cases/snapshots/cases__errors__fn_invalid_bound.snap new file mode 100644 index 0000000000..b52938fd5b --- /dev/null +++ b/crates/parser/tests/cases/snapshots/cases__errors__fn_invalid_bound.snap @@ -0,0 +1,12 @@ +--- +source: crates/parser/tests/cases/errors.rs +expression: "err_string(stringify!(fn_invalid_bound), module::parse_module,\n \"pub fn f() {}\")" + +--- +error: failed to parse generic bound + ┌─ fn_invalid_bound:1:12 + │ +1 │ pub fn f() {} + │ ^ expected a name, found symbol `(` + +