diff --git a/CHANGELOG.md b/CHANGELOG.md index 3421200e0d0..306767aa684 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -490,6 +490,10 @@ segments when compiling a function with a JavaScript external. ([Giacomo Cavalieri](https://github.com/giacomocavalieri)) +- Fixed a bug where the compiler would produce a confusing error message when + writing a constructor with a lowercase name. + ([Giacomo Cavalieri](https://github.com/giacomocavalieri)) + - A `gleam@@compile.erl` is no longer left in the build output of `gleam compile-package`. ([Louis Pilfold](https://github.com/lpil)) diff --git a/compiler-core/src/parse.rs b/compiler-core/src/parse.rs index 51541dc0a5e..b937f2cc9c1 100644 --- a/compiler-core/src/parse.rs +++ b/compiler-core/src/parse.rs @@ -255,7 +255,7 @@ where } fn parse_module(&mut self) -> Result { - let definitions = Parser::series_of(self, &Parser::parse_definition, None); + let definitions = self.series_of(&Parser::parse_definition, None); let definitions = self.ensure_no_errors_or_remaining_input(definitions)?; let module = Module { name: "".into(), @@ -613,9 +613,8 @@ where self.advance(); let _ = self .expect_one(&Token::LeftParen) - .map_err(|e| self.add_comment_style_hint(e))?; - let elements = - Parser::series_of(self, &Parser::parse_expression, Some(&Token::Comma))?; + .map_err(|error| self.add_comment_style_hint(error))?; + let elements = self.series_of(&Parser::parse_expression, Some(&Token::Comma))?; let (_, end) = self.expect_one_following_series(&Token::RightParen, "an expression")?; UntypedExpr::Tuple { @@ -732,11 +731,10 @@ where // BitArray Some((start, Token::LtLt, _)) => { self.advance(); - let segments = Parser::series_of( - self, - &|s| { + let segments = self.series_of( + &|this| { Parser::parse_bit_array_segment( - s, + this, &(|this| this.parse_expression_unit(ExpressionUnitContext::Other)), &Parser::expect_expression, &bit_array_expr_int, @@ -793,10 +791,9 @@ where // case Some((start, Token::Case, case_e)) => { self.advance(); - let subjects = - Parser::series_of(self, &Parser::parse_expression, Some(&Token::Comma))?; + let subjects = self.series_of(&Parser::parse_expression, Some(&Token::Comma))?; if self.maybe_one(&Token::LeftBrace).is_some() { - let clauses = Parser::series_of(self, &Parser::parse_case_clause, None)?; + let clauses = self.series_of(&Parser::parse_case_clause, None)?; let (_, end) = self.expect_one_following_series(&Token::RightBrace, "a case clause")?; if subjects.is_empty() { @@ -978,8 +975,7 @@ where }; let mut arguments = vec![]; if self.maybe_one(&Token::Comma).is_some() { - arguments = Parser::series_of( - self, + arguments = self.series_of( &Parser::parse_record_update_arg, Some(&Token::Comma), )?; @@ -1033,7 +1029,7 @@ where Some((_, Token::LArrow, _)) => { vec![] } - _ => Parser::series_of(self, &Parser::parse_use_assignment, Some(&Token::Comma))?, + _ => self.series_of(&Parser::parse_use_assignment, Some(&Token::Comma))?, }; _ = self.expect_one_following_series(&Token::LArrow, "a use variable assignment")?; @@ -1482,11 +1478,8 @@ where Some((start, Token::Hash, _)) => { self.advance(); let _ = self.expect_one(&Token::LeftParen)?; - let elements = Parser::series_of( - self, - &|this| this.parse_pattern(position), - Some(&Token::Comma), - )?; + let elements = + self.series_of(&|this| this.parse_pattern(position), Some(&Token::Comma))?; let (_, end) = self.expect_one_following_series(&Token::RightParen, "a pattern")?; Pattern::Tuple { location: SrcSpan { start, end }, @@ -1496,12 +1489,10 @@ where // BitArray Some((start, Token::LtLt, _)) => { self.advance(); - let segments = Parser::series_of( - self, - &|s| { - Parser::parse_bit_array_segment( - s, - &|s| match s.parse_pattern(position) { + let segments = self.series_of( + &|this| { + this.parse_bit_array_segment( + &|this| match this.parse_pattern(position) { Ok(Some(Pattern::BitArray { location, .. })) => { parse_error(ParseErrorType::NestedBitArrayPattern, location) } @@ -1549,8 +1540,7 @@ where if self.maybe_one(&Token::Comma).is_some() { // See if there's a list of items after the tail, // like `[..wibble, wobble, wabble]` - let elements = Parser::series_of( - self, + let elements = self.series_of( &|this| this.parse_pattern(position), Some(&Token::Comma), ); @@ -1690,7 +1680,7 @@ where let guard = self.parse_case_clause_guard()?; let (arr_s, arr_e) = self .expect_one(&Token::RArrow) - .map_err(|e| self.add_multi_line_clause_hint(e))?; + .map_err(|error| self.add_multi_line_clause_hint(error))?; let then = self.parse_expression()?; match then { Some(then) => Ok(Some(Clause { @@ -1727,11 +1717,7 @@ where &mut self, position: PatternPosition, ) -> Result, ParseError> { - Parser::series_of( - self, - &|this| this.parse_pattern(position), - Some(&Token::Comma), - ) + self.series_of(&|this| this.parse_pattern(position), Some(&Token::Comma)) } // examples: @@ -1829,7 +1815,7 @@ where end: l_paren_end, }, ) - .map_err(|e| self.add_multi_line_clause_hint(e)); + .map_err(|error| self.add_multi_line_clause_hint(error)); } Ok(()) @@ -2217,12 +2203,9 @@ where } let _ = self .expect_one(&Token::LeftParen) - .map_err(|e| self.add_anon_function_hint(e))?; - let arguments = Parser::series_of( - self, - &|parser| Parser::parse_fn_param(parser, is_anon), - Some(&Token::Comma), - )?; + .map_err(|error| self.add_anon_function_hint(error))?; + let arguments = + self.series_of(&|this| this.parse_fn_param(is_anon), Some(&Token::Comma))?; let (_, rpar_e) = self.expect_one_following_series(&Token::RightParen, "a function parameter")?; @@ -2448,8 +2431,7 @@ where // a: _, expr // a: expr, _, b: _ fn parse_fn_arguments(&mut self) -> Result, ParseError> { - let arguments = Parser::series_of(self, &Parser::parse_fn_argument, Some(&Token::Comma))?; - Ok(arguments) + self.series_of(&Parser::parse_fn_argument, Some(&Token::Comma)) } // Parse a single function call arg @@ -2567,90 +2549,65 @@ where let documentation = self.take_documentation(start); let (name_start, name, parameters, end, name_end) = self.expect_type_name()?; let name_location = SrcSpan::new(name_start, name_end); - let (constructors, end_position) = if self.maybe_one(&Token::LeftBrace).is_some() { - // Custom Type - let constructors = Parser::series_of( - self, - &|p| { - // The only attribute supported on constructors is @deprecated - let mut attributes = Attributes::default(); - let attr_loc = Parser::parse_attributes(p, &mut attributes)?; - - if let Some(attr_span) = attr_loc { - // Expecting all but the deprecated atterbutes to be default - if attributes.external_erlang.is_some() - || attributes.external_javascript.is_some() - || attributes.target.is_some() - || attributes.internal != InternalAttribute::Missing - { - return parse_error( - ParseErrorType::UnknownAttributeRecordVariant, - attr_span, - ); - } - } - match Parser::maybe_upname(p) { - Some((c_s, c_n, c_e)) => { - let documentation = p.take_documentation(c_s); - let (arguments, arguments_e) = - Parser::parse_type_constructor_arguments(p)?; - let end = arguments_e.max(c_e); - Ok(Some(RecordConstructor { - location: SrcSpan { start: c_s, end }, - name_location: SrcSpan { - start: c_s, - end: c_e, - }, - name: c_n, - arguments, - documentation, - deprecation: attributes.deprecated, - })) - } - _ => Ok(None), - } - }, - // No separator - None, - )?; - let (_, close_end) = self.expect_custom_type_close(&name, public, opaque)?; - (constructors, close_end) - } else { - match self.maybe_one(&Token::Equal) { - Some((eq_s, eq_e)) => { - // Type Alias - if opaque { - return parse_error( - ParseErrorType::OpaqueTypeAlias, - SrcSpan { start, end }, - ); - } + let (constructors, end_position) = match self.tok0.take() { + // If we see `type Wibble {`, then we know we're parsing a custom type. + Some((_, Token::LeftBrace, _)) => { + self.advance(); - match self.parse_type()? { - Some(t) => { - let type_end = t.location().end; - return Ok(Some(Definition::TypeAlias(TypeAlias { - documentation, - location: SrcSpan::new(start, type_end), - publicity: self.publicity(public, attributes.internal)?, - alias: name, - name_location, - parameters, - type_ast: t, - type_: (), - deprecation: std::mem::take(&mut attributes.deprecated), - }))); - } - _ => { - return parse_error( - ParseErrorType::ExpectedType, - SrcSpan::new(eq_s, eq_e), - ); - } - } + // If we see a lowercase name, rather than an uppercase one. We + // know there's a syntax error! So now we can try and provide a + // nice error message, based on what that wrong code looks like. + if let Some((name_start, Token::Name { .. }, name_end)) = &self.tok0 { + return Err(self.invalid_record_constructor_error( + name, + public, + opaque, + *name_start, + *name_end, + )); + } + + let constructors = self.series_of( + &|this| this.parse_record_constructor(), + // No separator + None, + )?; + let close_end = self.expect_custom_type_close()?; + (constructors, close_end) + } + + // If we see `type Wibble =` then we know we're parsing a type alias. + Some((equal_start, Token::Equal, equal_end)) => { + self.advance(); + + if opaque { + return parse_error(ParseErrorType::OpaqueTypeAlias, SrcSpan { start, end }); + } + + if let Some(type_) = self.parse_type()? { + return Ok(Some(Definition::TypeAlias(TypeAlias { + documentation, + location: SrcSpan::new(start, type_.location().end), + publicity: self.publicity(public, attributes.internal)?, + alias: name, + name_location, + parameters, + type_ast: type_, + type_: (), + deprecation: std::mem::take(&mut attributes.deprecated), + }))); + } else { + return parse_error( + ParseErrorType::ExpectedType, + SrcSpan::new(equal_start, equal_end), + ); } - _ => (vec![], end), + } + + token @ (Some(_) | None) => { + self.tok0 = token; + (vec![], end) } }; @@ -2671,6 +2628,188 @@ where }))) } + fn parse_record_constructor(&mut self) -> Result>, ParseError> { + // The only attribute supported on constructors is @deprecated + let mut attributes = Attributes::default(); + let attr_loc = self.parse_attributes(&mut attributes)?; + + if let Some(attr_span) = attr_loc { + // Expecting all but the deprecated atterbutes to be default + if attributes.external_erlang.is_some() + || attributes.external_javascript.is_some() + || attributes.target.is_some() + || attributes.internal != InternalAttribute::Missing + { + return parse_error(ParseErrorType::UnknownAttributeRecordVariant, attr_span); + } + } + + match self.maybe_upname() { + Some((name_start, constructor_name, name_end)) => { + let documentation = self.take_documentation(name_start); + let (arguments, arguments_end) = self.parse_record_constructor_arguments()?; + + Ok(Some(RecordConstructor { + location: SrcSpan { + start: name_start, + end: arguments_end.max(name_end), + }, + name_location: SrcSpan { + start: name_start, + end: name_end, + }, + name: constructor_name, + arguments, + documentation, + deprecation: attributes.deprecated, + })) + } + _ => Ok(None), + } + } + + /// This takes place when we find a lowercase name as a record constructor + /// variant (that name is passed as an argument here). + /// We want to look at the following tokens to produce a nice error message: + /// + /// ```gleam + /// pub type Wibble { + /// wibble + /// //^^^^^^ Error, this should be uppercase! + /// } + /// ``` + /// + /// But if the thing looks like a record definition, we want a specialised + /// error message: + /// + /// ```gleam + /// pub type Wibble { + /// wibble: Int, + /// wobble: String + /// } + /// // Suggest wrapping this in a constructor. + /// ``` + /// + fn invalid_record_constructor_error( + &mut self, + type_name: EcoString, + public: bool, + opaque: bool, + name_start: u32, + name_end: u32, + ) -> ParseError { + let fields = self.series_of( + &|this| this.parse_record_constructor_field(), + Some(&Token::Comma), + ); + + match fields { + // If there's a list of fields right inside the type that means the + // developer might have forgotten to wrap the thing in a constructor. + // Basically writing something like this: + // + // ```gleam + // pub type Wibble { + // String, + // wibble: Int, + // } + // ``` + // + // So we want to produce a specialised error message pointing them + // in the right direction. + Ok(fields) if let Some((_, Token::RightBrace, _)) = self.tok0 => ParseError { + location: SrcSpan { + start: fields + .first() + .map_or(name_start, |field| field.location.start), + end: fields.last().map_or(name_end, |field| field.location.end), + }, + error: ParseErrorType::ExpectedRecordConstructor { + type_name, + public, + opaque, + fields, + }, + }, + + // Otherwise we fall back to telling them the lowercase name should + // be uppercased! + Ok(_) | Err(_) => ParseError { + error: ParseErrorType::IncorrectUpName, + location: SrcSpan { + start: name_start, + end: name_end, + }, + }, + } + } + + // examples: + // *no args* + // () + // (a, b) + fn parse_record_constructor_arguments( + &mut self, + ) -> Result<(Vec>, u32), ParseError> { + if self.maybe_one(&Token::LeftParen).is_some() { + let arguments = self.series_of( + &|this| this.parse_record_constructor_field(), + Some(&Token::Comma), + )?; + let (_, end) = self + .expect_one_following_series(&Token::RightParen, "a constructor argument name")?; + Ok((arguments, end)) + } else { + Ok((vec![], 0)) + } + } + + fn parse_record_constructor_field( + &mut self, + ) -> Result>, ParseError> { + match (self.tok0.take(), self.tok1.take()) { + (Some((start, Token::Name { name }, name_end)), Some((_, Token::Colon, end))) => { + let _ = self.next_tok(); + let _ = self.next_tok(); + let doc = self.take_documentation(start); + match self.parse_type()? { + Some(type_ast) => { + let end = type_ast.location().end; + Ok(Some(RecordConstructorArg { + label: Some((SrcSpan::new(start, name_end), name)), + ast: type_ast, + location: SrcSpan { start, end }, + type_: (), + doc, + })) + } + None => parse_error(ParseErrorType::ExpectedType, SrcSpan { start, end }), + } + } + (t0, t1) => { + self.tok0 = t0; + self.tok1 = t1; + match self.parse_type()? { + Some(type_ast) => { + let doc = match &self.tok0 { + Some((start, _, _)) => self.take_documentation(*start), + None => None, + }; + let type_location = type_ast.location(); + Ok(Some(RecordConstructorArg { + label: None, + ast: type_ast, + location: type_location, + type_: (), + doc, + })) + } + None => Ok(None), + } + } + } + } + // examples: // A // A(one, two) @@ -2679,8 +2818,7 @@ where ) -> Result<(u32, EcoString, Vec, u32, u32), ParseError> { let (start, upname, end) = self.expect_upname()?; if let Some((par_s, _)) = self.maybe_one(&Token::LeftParen) { - let arguments = - Parser::series_of(self, &|p| Ok(Parser::maybe_name(p)), Some(&Token::Comma))?; + let arguments = self.series_of(&|this| Ok(this.maybe_name()), Some(&Token::Comma))?; let (_, par_e) = self.expect_one_following_series(&Token::RightParen, "a name")?; if arguments.is_empty() { return parse_error( @@ -2694,14 +2832,13 @@ where .collect(); Ok((start, upname, arguments2, par_e, end)) } else if let Some((less_start, less_end)) = self.maybe_one(&Token::Less) { - let mut arguments = Parser::series_of( - self, - &|p| + let mut arguments = self.series_of( + &|this| // Permit either names (`a`) or upnames (`A`) in this error-handling mode, // as upnames are common in other languages. Convert to lowercase so the // example is correct whichever was used. - Ok(Parser::maybe_name(p) - .or_else(|| Parser::maybe_upname(p)) + Ok(this.maybe_name() + .or_else(|| this.maybe_upname()) .map(|(_, name, _)| name.to_lowercase())), Some(&Token::Comma), )?; @@ -2727,72 +2864,6 @@ where } } - // examples: - // *no args* - // () - // (a, b) - fn parse_type_constructor_arguments( - &mut self, - ) -> Result<(Vec>, u32), ParseError> { - if self.maybe_one(&Token::LeftParen).is_some() { - let arguments = Parser::series_of( - self, - &|p| match (p.tok0.take(), p.tok1.take()) { - ( - Some((start, Token::Name { name }, name_end)), - Some((_, Token::Colon, end)), - ) => { - let _ = Parser::next_tok(p); - let _ = Parser::next_tok(p); - let doc = p.take_documentation(start); - match Parser::parse_type(p)? { - Some(type_ast) => { - let end = type_ast.location().end; - Ok(Some(RecordConstructorArg { - label: Some((SrcSpan::new(start, name_end), name)), - ast: type_ast, - location: SrcSpan { start, end }, - type_: (), - doc, - })) - } - None => { - parse_error(ParseErrorType::ExpectedType, SrcSpan { start, end }) - } - } - } - (t0, t1) => { - p.tok0 = t0; - p.tok1 = t1; - match Parser::parse_type(p)? { - Some(type_ast) => { - let doc = match &p.tok0 { - Some((start, _, _)) => p.take_documentation(*start), - None => None, - }; - let type_location = type_ast.location(); - Ok(Some(RecordConstructorArg { - label: None, - ast: type_ast, - location: type_location, - type_: (), - doc, - })) - } - None => Ok(None), - } - } - }, - Some(&Token::Comma), - )?; - let (_, end) = self - .expect_one_following_series(&Token::RightParen, "a constructor argument name")?; - Ok((arguments, end)) - } else { - Ok((vec![], 0)) - } - } - // // Parse Type Annotations // @@ -2841,8 +2912,7 @@ where Some((start, Token::Fn, _)) => { self.advance(); let _ = self.expect_one(&Token::LeftParen)?; - let arguments = - Parser::series_of(self, &|x| Parser::parse_type(x), Some(&Token::Comma))?; + let arguments = self.series_of(&|this| this.parse_type(), Some(&Token::Comma))?; let _ = self.expect_one_following_series(&Token::RightParen, "a type")?; let (arr_s, arr_e) = self.expect_one(&Token::RArrow)?; let return_ = self.parse_type()?; @@ -2990,7 +3060,7 @@ where // For parsing a comma separated "list" of types, for tuple, constructor, and function fn parse_types(&mut self) -> Result, ParseError> { - let elements = Parser::series_of(self, &|p| Parser::parse_type(p), Some(&Token::Comma))?; + let elements = self.series_of(&|this| this.parse_type(), Some(&Token::Comma))?; Ok(elements) } @@ -3303,8 +3373,7 @@ where Some((start, Token::Hash, _)) => { self.advance(); let _ = self.expect_one(&Token::LeftParen)?; - let elements = - Parser::series_of(self, &Parser::parse_const_value, Some(&Token::Comma))?; + let elements = self.series_of(&Parser::parse_const_value, Some(&Token::Comma))?; let (_, end) = self.expect_one_following_series(&Token::RightParen, "a constant value")?; Ok(Some(Constant::Tuple { @@ -3420,8 +3489,7 @@ where // BitArray Some((start, Token::LtLt, _)) => { self.advance(); - let segments = Parser::series_of( - self, + let segments = self.series_of( &|this| { this.parse_bit_array_segment( &Parser::parse_const_value, @@ -3586,8 +3654,7 @@ where let mut update_arguments = vec![]; if self.maybe_one(&Token::Comma).is_some() { - update_arguments = Parser::series_of( - self, + update_arguments = self.series_of( &Parser::parse_const_record_update_arg, Some(&Token::Comma), )?; @@ -3611,11 +3678,8 @@ where field_map: Inferred::Unknown, })) } else { - let arguments = Parser::series_of( - self, - &Parser::parse_const_record_arg, - Some(&Token::Comma), - )?; + let arguments = + self.series_of(&Parser::parse_const_record_arg, Some(&Token::Comma))?; let (_, par_e) = self.expect_one_following_series( &Token::RightParen, @@ -3799,8 +3863,7 @@ where match value_parser(self)? { Some(value) => { let options = if self.maybe_one(&Token::Colon).is_some() { - Parser::series_of( - self, + self.series_of( &|this| this.parse_bit_array_option(&arg_parser, &to_int_segment), Some(&Token::Minus), )? @@ -4046,57 +4109,29 @@ where /// Expect the end to a custom type definiton or handle an incorrect /// record constructor definition. - /// - /// Used for mapping to a more specific error type and message. - fn expect_custom_type_close( - &mut self, - name: &EcoString, - public: bool, - opaque: bool, - ) -> Result<(u32, u32), ParseError> { + fn expect_custom_type_close(&mut self) -> Result { match self.maybe_one(&Token::RightBrace) { - Some((start, end)) => Ok((start, end)), + Some((_, end)) => Ok(end), None => match self.next_tok() { None => parse_error(ParseErrorType::UnexpectedEof, SrcSpan { start: 0, end: 0 }), Some((start, token, end)) => { - // If provided a Name, map to a more detailed error - // message to nudge the user. - // Else, handle as an unexpected token. - let field = if let Token::Name { name } = token { - name - } else { - let hint = match (&token, self.tok0.take()) { - (&Token::Fn, _) | (&Token::Pub, Some((_, Token::Fn, _))) => { - let text = "Gleam is not an object oriented programming language so + let hint = match (&token, self.tok0.take()) { + (&Token::Fn, _) | (&Token::Pub, Some((_, Token::Fn, _))) => { + let text = "Gleam is not an object oriented programming language so functions are declared separately from types."; - Some(wrap(text).into()) - } - (_, _) => None, - }; - - return parse_error( - ParseErrorType::UnexpectedToken { - token, - expected: vec![ - Token::RightBrace.to_string().into(), - "a record constructor".into(), - ], - hint, - }, - SrcSpan { start, end }, - ); - }; - let field_type = match self.parse_type_annotation(&Token::Colon) { - Ok(Some(annotation)) => Some(Box::new(annotation)), - _ => None, + Some(wrap(text).into()) + } + (_, _) => None, }; + parse_error( - ParseErrorType::ExpectedRecordConstructor { - name: name.clone(), - public, - opaque, - field, - field_type, + ParseErrorType::UnexpectedToken { + token, + expected: vec![ + Token::RightBrace.to_string().into(), + "a record constructor".into(), + ], + hint, }, SrcSpan { start, end }, ) diff --git a/compiler-core/src/parse/error.rs b/compiler-core/src/parse/error.rs index df01dda3eb7..31a664ace06 100644 --- a/compiler-core/src/parse/error.rs +++ b/compiler-core/src/parse/error.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2020 The Gleam contributors -use crate::ast::{SrcSpan, TypeAst}; +use crate::ast::{RecordConstructorArg, SrcSpan, TypeAst}; use crate::diagnostic::{ExtraLabel, Label}; use crate::error::{wrap, wrap_format}; use crate::parse::Token; @@ -145,12 +145,23 @@ pub enum ParseErrorType { RedundantInternalAttribute, // for a private definition marked as internal InvalidModuleTypePattern, // for patterns that have a dot like: `name.thing` ListPatternSpreadFollowedByElements, // When there is a pattern after a spread [..rest, pattern] + + /// This happens when someone forgets to write the type constructor around + /// its fields in a custom type definition. For example: + /// + /// ```gleam + /// pub type Wibble { + /// String, + /// field: Int, + /// field_2: a, + /// } + /// ``` ExpectedRecordConstructor { - name: EcoString, + type_name: EcoString, public: bool, opaque: bool, - field: EcoString, - field_type: Option>, + /// Those are the fields that have been written. + fields: Vec>, }, CallInClauseGuard, // case x { _ if f() -> 1 } IfExpression, @@ -684,11 +695,10 @@ See: https://tour.gleam.run/flow-control/case-expressions/" }, ParseErrorType::ExpectedRecordConstructor { - name, + type_name, public, opaque, - field, - field_type, + fields, } => { let (accessor, opaque) = match *public { true if *opaque => ("pub ", "opaque "), @@ -696,22 +706,29 @@ See: https://tour.gleam.run/flow-control/case-expressions/" false => ("", ""), }; - let mut annotation = EcoString::new(); - match field_type { - Some(t) => t.print(&mut annotation), - None => annotation.push_str("Type"), - }; + let fields = fields + .iter() + .map(|field| { + let mut type_ = EcoString::new(); + field.ast.print(&mut type_); + + match field.label.as_ref() { + Some((_, label)) => format!(" {label}: {type_},"), + None => format!(" {type_},"), + } + }) + .join("\n"); ParseErrorDetails { - text: [ - "Each custom type variant must have a constructor:\n".into(), - format!("{accessor}{opaque}type {name} {{"), - format!(" {name}("), - format!(" {field}: {annotation},"), - " )".into(), - "}".into(), - ] - .join("\n"), + text: format!( + "Each custom type variant must have a constructor: + +{accessor}{opaque}type {type_name} {{ + {type_name}( +{fields} + ) +}}" + ), hint: None, label_text: "I was not expecting this".into(), extra_labels: vec![], diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__lowercase_constructor_name_in_custom_type.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__lowercase_constructor_name_in_custom_type.snap new file mode 100644 index 00000000000..edd6057cdcb --- /dev/null +++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__lowercase_constructor_name_in_custom_type.snap @@ -0,0 +1,20 @@ +--- +source: compiler-core/src/parse/tests.rs +expression: "\npub type Wibble {\n wibble(Int, String)\n}\n" +--- +----- SOURCE CODE + +pub type Wibble { + wibble(Int, String) +} + + +----- ERROR +error: Syntax error + ┌─ /src/parse/error.gleam:3:3 + │ +3 │ wibble(Int, String) + │ ^^^^^^ I'm expecting a type name here + +Hint: Type names start with a uppercase letter, and can contain a-z, A-Z, +or 0-9. diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__missing_constructor_name_with_multiple_fields.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__missing_constructor_name_with_multiple_fields.snap new file mode 100644 index 00000000000..48304dc6ad8 --- /dev/null +++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__missing_constructor_name_with_multiple_fields.snap @@ -0,0 +1,28 @@ +--- +source: compiler-core/src/parse/tests.rs +expression: "\npub type Wibble(a) {\n field: Int,\n other: a\n}\n" +--- +----- SOURCE CODE + +pub type Wibble(a) { + field: Int, + other: a +} + + +----- ERROR +error: Syntax error + ┌─ /src/parse/error.gleam:3:3 + │ +3 │ ╭ field: Int, +4 │ │ other: a + │ ╰──────────^ I was not expecting this + +Each custom type variant must have a constructor: + +pub type Wibble { + Wibble( + field: Int, + other: a, + ) +} diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__type_invalid_record_constructor.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__type_invalid_record_constructor.snap index 06207571fad..54fa33b940d 100644 --- a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__type_invalid_record_constructor.snap +++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__type_invalid_record_constructor.snap @@ -14,7 +14,7 @@ error: Syntax error ┌─ /src/parse/error.gleam:3:5 │ 3 │ name: String, - │ ^^^^ I was not expecting this + │ ^^^^^^^^^^^^ I was not expecting this Each custom type variant must have a constructor: diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__type_invalid_record_constructor_invalid_field_type.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__type_invalid_record_constructor_invalid_field_type.snap index c88b13e02d0..4581720e576 100644 --- a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__type_invalid_record_constructor_invalid_field_type.snap +++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__type_invalid_record_constructor_invalid_field_type.snap @@ -14,12 +14,7 @@ error: Syntax error ┌─ /src/parse/error.gleam:3:5 │ 3 │ name: "Test User", - │ ^^^^ I was not expecting this + │ ^^^^ I'm expecting a type name here -Each custom type variant must have a constructor: - -type User { - User( - name: Type, - ) -} +Hint: Type names start with a uppercase letter, and can contain a-z, A-Z, +or 0-9. diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__type_invalid_record_constructor_without_field_type.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__type_invalid_record_constructor_without_field_type.snap index 96fa42ffebb..44529f2f4f5 100644 --- a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__type_invalid_record_constructor_without_field_type.snap +++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__type_invalid_record_constructor_without_field_type.snap @@ -20,6 +20,6 @@ Each custom type variant must have a constructor: pub opaque type User { User( - name: Type, + name, ) } diff --git a/compiler-core/src/parse/tests.rs b/compiler-core/src/parse/tests.rs index fdd91867fa1..579c5e8258d 100644 --- a/compiler-core/src/parse/tests.rs +++ b/compiler-core/src/parse/tests.rs @@ -2510,3 +2510,26 @@ fn error_message_when_using_discard_pattern_for_as_pattern() { }" ); } + +#[test] +fn missing_constructor_name_with_multiple_fields() { + assert_module_error!( + r#" +pub type Wibble(a) { + field: Int, + other: a +} +"# + ); +} + +#[test] +fn lowercase_constructor_name_in_custom_type() { + assert_module_error!( + r#" +pub type Wibble { + wibble(Int, String) +} +"# + ); +}