diff --git a/src/erl.rs b/src/erl.rs index 897545cdcdd..a9c373e12e7 100644 --- a/src/erl.rs +++ b/src/erl.rs @@ -118,7 +118,7 @@ pub fn records(module: &TypedModule) -> Vec<(&str, String)> { pub fn record_definition(name: &str, fields: &[&str]) -> String { let name = &name.to_snake_case(); - let escaped_name = if is_reserved_word(name) { + let escaped_name = if is_erlang_reserved_word(name) { format!("'{}'", name) } else { format!("{}", name) @@ -126,7 +126,7 @@ pub fn record_definition(name: &str, fields: &[&str]) -> String { use std::fmt::Write; let mut buffer = format!("-record({}, {{", escaped_name); for field in fields.iter().intersperse(&", ") { - let escaped_field = if is_reserved_word(field) { + let escaped_field = if is_erlang_reserved_word(field) { format!("'{}'", field) } else { format!("{}", field) @@ -265,7 +265,7 @@ fn atom(value: String) -> Document { match &*value { // Escape because of keyword collision - value if is_reserved_word(value) => format!("'{}'", value).to_doc(), + value if is_erlang_reserved_word(value) => format!("'{}'", value).to_doc(), // No need to escape _ if RE.is_match(&value) => value.to_doc(), @@ -1232,7 +1232,7 @@ fn external_fun(name: &str, module: &str, fun: &str, arity: usize) -> Document { .nest(INDENT) } -fn is_reserved_word(name: &str) -> bool { +pub fn is_erlang_reserved_word(name: &str) -> bool { return match name { "!" | "receive" | "bnot" | "div" | "rem" | "band" | "bor" | "bxor" | "bsl" | "bsr" | "not" | "and" | "or" | "xor" | "orelse" | "andalso" | "when" | "end" | "fun" | "try" diff --git a/src/error.rs b/src/error.rs index 366e8d0739b..ae7cd4a8470 100644 --- a/src/error.rs +++ b/src/error.rs @@ -139,6 +139,18 @@ pub enum Error { command: String, err: Option, }, + + InvalidProjectName { + name: String, + reason: InvalidProjectNameReason, + }, +} + +#[derive(Debug, PartialEq)] +pub enum InvalidProjectNameReason { + Format, + ErlangReservedWord, + GleamReservedWord, } #[derive(Debug, PartialEq)] @@ -223,6 +235,29 @@ impl Error { .expect("error pretty buffer write space before"); match self { + Error::InvalidProjectName { name, reason } => { + let diagnostic = ProjectErrorDiagnostic { + title: "Invalid project name".to_string(), + label: format!( + "We were not able to create your project as `{}` is +{} + +Please try again with a different project name.", + name, + match reason { + InvalidProjectNameReason::ErlangReservedWord => + "a reserved word in Erlang.", + InvalidProjectNameReason::GleamReservedWord => + "a reserved word in Gleam.", + InvalidProjectNameReason::Format => + "does not have the correct format. Project names must start +with a lowercase latter and may only contain lowercase letters +and underscores.", + } + ), + }; + write_project(buffer, diagnostic); + } Error::ShellCommand { command, err: None } => { let diagnostic = ProjectErrorDiagnostic { title: "Shell command failure".to_string(), diff --git a/src/new.rs b/src/new.rs index 0bd6790abb5..c24c02d8829 100644 --- a/src/new.rs +++ b/src/new.rs @@ -1,4 +1,4 @@ -use crate::error::{Error, FileIOAction, FileKind, GleamExpect}; +use crate::error::{Error, FileIOAction, FileKind, GleamExpect, InvalidProjectNameReason}; use serde::{Deserialize, Serialize}; use std::fs::File; use std::io::Write; @@ -20,14 +20,7 @@ pub fn create( path: Option, version: &'static str, ) -> Result<(), Error> { - if !regex::Regex::new("^[a-z_]+$") - .gleam_expect("new name regex could not be compiled") - .is_match(&name) - { - // TODO - println!("error: Project name may only contain lowercase letters and underscores"); - std::process::exit(1); - } + let name = validate_name(name)?; let description = description.unwrap_or_else(|| String::from("A Gleam program")); let path = path.unwrap_or_else(|| name.clone()); @@ -510,3 +503,27 @@ fn app_rebar_config(name: &str) -> String { name )) } + +fn validate_name(name: String) -> Result { + if crate::erl::is_erlang_reserved_word(name.as_str()) { + Err(Error::InvalidProjectName { + name, + reason: InvalidProjectNameReason::ErlangReservedWord, + }) + } else if crate::parser::is_gleam_reserved_word(name.as_str()) { + Err(Error::InvalidProjectName { + name, + reason: InvalidProjectNameReason::GleamReservedWord, + }) + } else if !regex::Regex::new("^[a-z_]+$") + .gleam_expect("new name regex could not be compiled") + .is_match(&name) + { + Err(Error::InvalidProjectName { + name, + reason: InvalidProjectNameReason::Format, + }) + } else { + Ok(name) + } +} diff --git a/src/parser.rs b/src/parser.rs index 015b89b007f..c23a4169d8a 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -497,3 +497,10 @@ pub fn make_call( }), } } + +pub fn is_gleam_reserved_word(s: &str) -> bool { + match s { + "pub" | "fn" | "import" | "as" | "type" | "extern" | "case" | "try" | "assert" => true, + _ => false, + } +}