Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to Gleam 1.3.2 #12

Merged
merged 16 commits into from
Jul 28, 2024
11 changes: 11 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,17 @@ jobs:
cd lib_project
glistix run --target erlang
glistix test --target erlang

# Test adding of deps
glistix add exception # No specifier
glistix add gleam_http@3 # Version specifier
glistix test --target erlang

# Test documentation generation
# Workaround for lack of --target option for docs build
# TODO: Remove this workaround after Gleam 1.4
# (See https://github.com/gleam-lang/gleam/pull/3333)
sed -i -e 's/target = "nix"/target = "erlang"/' gleam.toml
glistix docs build

# Assert that module metadata has been written
Expand Down
24 changes: 23 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,31 @@

### Bug Fixes

## v1.3.2 - 2024-07-11

### Language Server

- The language server no longer shows completions when inside a literal string.
([Giacomo Cavalieri](https://github.com/giacomocavalieri))

### Bug Fixes

- Fixed a bug where the compiler would report errors for duplicate `@external`
attributes with inconsistent spans between Erlang and JavaScript.
([Connor Szczepaniak](https://github.com/cszczepaniak))

- Fixed a bug where `gleam add` would fail to parse version specifiers
correctly.
([Louis Pilfold](https://github.com/lpil))

- Fixed a bug where single clause case expressions could generate JavaScript
code with incorrectly rewritten JavaScript variable names.
([Louis Pilfold](https://github.com/lpil))

## v1.3.1 - 2024-07-10

### Bug Fixes

- Fixes a bug with import cycle detection when there is more than 2 imports in the cycle
- Fixes a bug with import cycle detection when there is more than 2 imports in
the cycle.
([Ameen Radwan](https://github.com/Acepie))
29 changes: 20 additions & 9 deletions compiler-cli/src/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,26 @@ use glistix_core::{
Error, Result,
};

use crate::{cli, dependencies::UseManifest, fs};
use crate::{
cli,
dependencies::{parse_gleam_add_specifier, UseManifest},
fs,
};

pub fn command(packages: Vec<String>, dev: bool) -> Result<()> {
pub fn command(packages_to_add: Vec<String>, dev: bool) -> Result<()> {
let paths = crate::find_project_paths()?;

let mut new_package_requirements = Vec::with_capacity(packages_to_add.len());
for specifier in packages_to_add {
new_package_requirements.push(parse_gleam_add_specifier(&specifier)?);
}

// Insert the new packages into the manifest and perform dependency
// resolution to determine suitable versions
let manifest = crate::dependencies::download(
&paths,
cli::Reporter::new(),
Some((packages.to_vec(), dev)),
Some((new_package_requirements.clone(), dev)),
UseManifest::Yes,
)?;

Expand All @@ -24,12 +33,14 @@ pub fn command(packages: Vec<String>, dev: bool) -> Result<()> {
let mut manifest_toml = read_toml_edit("manifest.toml")?;

// Insert the new deps
for package_to_add in packages {
for (added_package, _) in new_package_requirements {
let added_package = added_package.to_string();

// Pull the selected version out of the new manifest so we know what it is
let version = &manifest
.packages
.iter()
.find(|package| package.name == *package_to_add)
.find(|package| package.name == *added_package)
.expect("Added package not found in resolved manifest")
.version;

Expand All @@ -49,16 +60,16 @@ pub fn command(packages: Vec<String>, dev: bool) -> Result<()> {
#[allow(clippy::indexing_slicing)]
{
if dev {
gleam_toml["dev-dependencies"][&package_to_add] = toml_edit::value(range.clone());
gleam_toml["dev-dependencies"][&added_package] = toml_edit::value(range.clone());
} else {
gleam_toml["dependencies"][&package_to_add] = toml_edit::value(range.clone());
gleam_toml["dependencies"][&added_package] = toml_edit::value(range.clone());
};
manifest_toml["requirements"][&package_to_add]
manifest_toml["requirements"][&added_package]
.as_inline_table_mut()
.expect("Invalid manifest format")["version"] = range.into();
}

cli::print_added(&format!("{package_to_add} v{version}"));
cli::print_added(&format!("{added_package} v{version}"));
}

// Write the updated config
Expand Down
151 changes: 74 additions & 77 deletions compiler-cli/src/dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,133 +119,131 @@ pub fn update() -> Result<()> {
Ok(())
}

fn parse_hex_requirement(package: &str) -> Result<Requirement> {
match package.find('@') {
Some(pos) => {
// Parse the major and minor from the provided semantic version.
let version = match package.get(pos + 1..) {
Some(version) => Ok(version),
None => Err(Error::InvalidVersionFormat {
input: package.to_string(),
error: "Failed to parse version from specifier".to_string(),
}),
}?;
let parts = version.split('.').collect::<Vec<_>>();
let major = match parts.first() {
Some(major) => Ok(major),
None => Err(Error::InvalidVersionFormat {
input: package.to_string(),
error: "Failed to parse semantic major version".to_string(),
}),
}?;
let minor = match parts.get(1) {
Some(minor) => minor,
None => "0",
};
pub fn parse_gleam_add_specifier(package: &str) -> Result<(EcoString, Requirement)> {
let Some((package, version)) = package.split_once('@') else {
// Default to the latest version available.
return Ok((package.into(), Requirement::hex(">= 0.0.0")));
};

// Using the major version specifier, calculate the maximum
// allowable version (i.e., the next major version).
let max_ver = match major.parse::<usize>() {
Ok(num) => Ok([&(num + 1).to_string(), "0", "0"].join(".")),
Err(_) => Err(Error::InvalidVersionFormat {
input: version.to_string(),
error: "Failed to parse semantic major version as integer".to_string(),
}),
}?;

// Pad the provided version specifier with zeros map to a Hex version.
match parts.len() {
1 | 2 => {
let min_ver = [major, minor, "0"].join(".");
Ok(Requirement::hex(
&[">=", &min_ver, "and", "<", &max_ver].join(" "),
))
}
3 => Ok(Requirement::hex(version)),
n_parts => Err(Error::InvalidVersionFormat {
input: version.to_string(),
error: format!(
"Expected up to 3 numbers in version specifier (MAJOR.MINOR.PATCH), found {n_parts}"
),
}),
}
// Parse the major and minor from the provided semantic version.
let parts = version.split('.').collect::<Vec<_>>();
let major = match parts.first() {
Some(major) => Ok(major),
None => Err(Error::InvalidVersionFormat {
input: package.to_string(),
error: "Failed to parse semantic major version".to_string(),
}),
}?;
let minor = match parts.get(1) {
Some(minor) => minor,
None => "0",
};

// Using the major version specifier, calculate the maximum
// allowable version (i.e., the next major version).
let Ok(num) = major.parse::<usize>() else {
return Err(Error::InvalidVersionFormat {
input: version.to_string(),
error: "Failed to parse semantic major version as integer".to_string(),
});
};

let max_ver = [&(num + 1).to_string(), "0", "0"].join(".");

// Pad the provided version specifier with zeros map to a Hex version.
let requirement = match parts.len() {
1 | 2 => {
let min_ver = [major, minor, "0"].join(".");
Requirement::hex(&[">=", &min_ver, "and", "<", &max_ver].join(" "))
}
3 => Requirement::hex(version),
n => {
return Err(Error::InvalidVersionFormat {
input: version.to_string(),
error: format!(
"Expected up to 3 numbers in version specifier (MAJOR.MINOR.PATCH), found {n}"
),
})
}
};

// Default to the latest version available.
None => Ok(Requirement::hex(">= 0.0.0")),
}
Ok((package.into(), requirement))
}

#[test]
fn parse_hex_requirement_invalid_semver() {
assert!(parse_hex_requirement("[email protected]").is_err());
fn parse_gleam_add_specifier_invalid_semver() {
assert!(parse_gleam_add_specifier("[email protected]").is_err());
}

#[test]
fn parse_hex_requirement_non_numeric_version() {
assert!(parse_hex_requirement("some_package@not_a_version").is_err());
fn parse_gleam_add_specifier_non_numeric_version() {
assert!(parse_gleam_add_specifier("some_package@not_a_version").is_err());
}

#[test]
fn parse_hex_requirement_default() {
fn parse_gleam_add_specifier_default() {
let provided = "some_package";
let expected = ">= 0.0.0";
let version = parse_hex_requirement(&provided).unwrap();
let (package, version) = parse_gleam_add_specifier(provided).unwrap();
match &version {
Requirement::Hex { version: v } => {
assert!(v.to_pubgrub().is_ok(), "failed pubgrub parse: {}", v);
}
_ => assert!(false, "failed hexpm version parse: {}", provided),
}
assert_eq!(version, Requirement::hex(expected))
assert_eq!(version, Requirement::hex(expected));
assert_eq!("some_package", package);
}

#[test]
fn parse_hex_requirement_major_only() {
let provided = "some_package@1";
fn parse_gleam_add_specifier_major_only() {
let provided = "wobble@1";
let expected = ">= 1.0.0 and < 2.0.0";
let version = parse_hex_requirement(&provided).unwrap();
let (package, version) = parse_gleam_add_specifier(provided).unwrap();
match &version {
Requirement::Hex { version: v } => {
assert!(v.to_pubgrub().is_ok(), "failed pubgrub parse: {}", v);
}
_ => assert!(false, "failed hexpm version parse: {}", provided),
}
assert_eq!(version, Requirement::hex(expected))
assert_eq!(version, Requirement::hex(expected));
assert_eq!("wobble", package);
}

#[test]
fn parse_hex_requirement_major_and_minor() {
let provided = "some_package@1.2";
fn parse_gleam_add_specifier_major_and_minor() {
let provided = "wibble@1.2";
let expected = ">= 1.2.0 and < 2.0.0";
let version = parse_hex_requirement(&provided).unwrap();
let (package, version) = parse_gleam_add_specifier(provided).unwrap();
match &version {
Requirement::Hex { version: v } => {
assert!(v.to_pubgrub().is_ok(), "failed pubgrub parse: {}", v);
}
_ => assert!(false, "failed hexpm version parse: {}", provided),
}
assert_eq!(version, Requirement::hex(expected))
assert_eq!(version, Requirement::hex(expected));
assert_eq!("wibble", package);
}

#[test]
fn parse_hex_requirement_major_minor_and_patch() {
let provided = "some_package@1.2.3";
fn parse_gleam_add_specifier_major_minor_and_patch() {
let provided = "bobble@1.2.3";
let expected = "1.2.3";
let version = parse_hex_requirement(&provided).unwrap();
let (package, version) = parse_gleam_add_specifier(provided).unwrap();
match &version {
Requirement::Hex { version: v } => {
assert!(v.to_pubgrub().is_ok(), "failed pubgrub parse: {}", v);
}
_ => assert!(false, "failed hexpm version parse: {}", provided),
}
assert_eq!(version, Requirement::hex(expected))
assert_eq!(version, Requirement::hex(expected));
assert_eq!("bobble", package);
}

pub fn download<Telem: Telemetry>(
paths: &ProjectPaths,
telemetry: Telem,
new_package: Option<(Vec<String>, bool)>,
new_package: Option<(Vec<(EcoString, Requirement)>, bool)>,
// If true we read the manifest from disc. If not set then we ignore any
// manifest which will result in the latest versions of the dependency
// packages being resolved (not the locked ones).
Expand All @@ -271,12 +269,11 @@ pub fn download<Telem: Telemetry>(

// Insert the new packages to add, if it exists
if let Some((packages, dev)) = new_package {
for package in packages {
let version = parse_hex_requirement(&package)?;
let _ = if dev {
config.dev_dependencies.insert(package.into(), version)
for (package, requirement) in packages {
if dev {
_ = config.dev_dependencies.insert(package, requirement);
} else {
config.dependencies.insert(package.into(), version)
_ = config.dependencies.insert(package, requirement);
};
}
}
Expand Down
31 changes: 16 additions & 15 deletions compiler-core/src/javascript/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,23 +366,10 @@ impl<'module> Generator<'module> {
self.scope_position = scope_position;

// Wrap in iife document
let doc = self.immediately_involked_function_expression_document(result?);
let doc = immediately_involked_function_expression_document(result?);
Ok(self.wrap_return(doc))
}

/// Wrap a document in an immediately involked function expression
fn immediately_involked_function_expression_document<'a>(
&mut self,
document: Document<'a>,
) -> Document<'a> {
docvec!(
docvec!("(() => {", break_("", " "), document).nest(INDENT),
break_("", " "),
"})()",
)
.group()
}

fn variable<'a>(
&mut self,
name: &'a EcoString,
Expand Down Expand Up @@ -631,7 +618,11 @@ impl<'module> Generator<'module> {
doc = if is_only_clause {
// If this is the only clause and there are no checks then we can
// render just the body as the case does nothing
doc.append(body)
// A block is used as it could declare variables still.
doc.append("{")
.append(docvec!(line(), body).nest(INDENT))
.append(line())
.append("}")
} else if is_final_clause {
// If this is the final clause and there are no checks then we can
// render `else` instead of `else if (...)`
Expand Down Expand Up @@ -1602,3 +1593,13 @@ fn requires_semicolon(statement: &TypedStatement) -> bool {
Statement::Use(_) => false,
}
}

/// Wrap a document in an immediately involked function expression
fn immediately_involked_function_expression_document(document: Document<'_>) -> Document<'_> {
docvec!(
docvec!("(() => {", break_("", " "), document).nest(INDENT),
break_("", " "),
"})()",
)
.group()
}
Loading
Loading