diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e75731310b..3409227fcf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -147,6 +147,10 @@ is now smaller in certain cases. ([Surya Rose](https://github.com/GearsDatapacks)) +- The compiler now emits a warning when a module contains no public definitions + and prevents publishing packages with empty modules to Hex. + ([Vitor Souza](https://github.com/vit0rr)) + ### Build tool - New projects are generated using OTP28 on GitHub Actions. diff --git a/compiler-cli/src/publish.rs b/compiler-cli/src/publish.rs index d1dff790e32..5673a4d011f 100644 --- a/compiler-cli/src/publish.rs +++ b/compiler-cli/src/publish.rs @@ -378,6 +378,31 @@ fn do_build_hex_tarball(paths: &ProjectPaths, config: &mut PackageConfig) -> Res }); } + // empty_modules is a list of modules that do not export any values or types. + // We do not allow publishing packages that contain empty modules. + let empty_modules: Vec<_> = built + .root_package + .modules + .iter() + .filter(|module| { + built + .module_interfaces + .get(&module.name) + .map(|interface| { + // Check if the module exports any values or types + interface.values.is_empty() && interface.types.is_empty() + }) + .unwrap_or(false) + }) + .map(|module| module.name.clone()) + .collect(); + + if !empty_modules.is_empty() { + return Err(Error::CannotPublishEmptyModules { + unfinished: empty_modules, + }); + } + // TODO: If any of the modules in the package contain a leaked internal type then // refuse to publish as the package is not yet finished. // We need to move aliases in to the type system first. diff --git a/compiler-core/src/ast.rs b/compiler-core/src/ast.rs index ceb1ace697a..c146de40160 100644 --- a/compiler-core/src/ast.rs +++ b/compiler-core/src/ast.rs @@ -868,6 +868,16 @@ impl TypedDefinition { } } + pub fn is_public(&self) -> bool { + match self { + Definition::Function(f) => f.publicity.is_public(), + Definition::TypeAlias(t) => t.publicity.is_public(), + Definition::CustomType(t) => t.publicity.is_public(), + Definition::ModuleConstant(c) => c.publicity.is_public(), + Definition::Import(_) => false, + } + } + pub fn find_node(&self, byte_index: u32) -> Option> { match self { Definition::Function(function) => { diff --git a/compiler-core/src/build/package_compiler.rs b/compiler-core/src/build/package_compiler.rs index 778d105d332..67dcf9cd255 100644 --- a/compiler-core/src/build/package_compiler.rs +++ b/compiler-core/src/build/package_compiler.rs @@ -569,6 +569,21 @@ fn analyse( // Register the types from this module so they can be imported into // other modules. let _ = module_types.insert(module.name.clone(), module.ast.type_info.clone()); + + // Check for empty modules and emit warning + // Only emit the empty module warning if the module has no definitions at all. + // Modules with only private definitions already emit their own warnings. + if module_types + .get(&module.name) + .map(|interface| interface.values.is_empty() && interface.types.is_empty()) + .unwrap_or(false) + { + warnings.emit(crate::warning::Warning::EmptyModule { + path: module.input_path.clone(), + name: module.name.clone(), + }); + } + // Register the successfully type checked module data so that it can be // used for code generation and in the language server. modules.push(module); diff --git a/compiler-core/src/error.rs b/compiler-core/src/error.rs index d2e3e7f308b..b8e615b21bf 100644 --- a/compiler-core/src/error.rs +++ b/compiler-core/src/error.rs @@ -298,6 +298,9 @@ file_names.iter().map(|x| x.as_str()).join(", "))] )] CannotPublishLeakedInternalType { unfinished: Vec }, + #[error("The modules {unfinished:?} are empty and so cannot be published")] + CannotPublishEmptyModules { unfinished: Vec }, + #[error("Publishing packages to reserve names is not permitted")] HexPackageSquatting, @@ -1047,6 +1050,24 @@ Please make sure internal types do not appear in public functions and try again. location: None, }], + Error::CannotPublishEmptyModules { unfinished } => vec![Diagnostic { + title: "Cannot publish empty modules".into(), + text: wrap_format!( + "These modules contain no public definitions and cannot be published: + +{} + +Please add public functions, types, or constants to these modules, or remove them and try again.", + unfinished + .iter() + .map(|name| format!(" - {}", name.as_str())) + .join("\n") + ), + level: Level::Error, + hint: None, + location: None, + }], + Error::UnableToFindProjectRoot { path } => { let text = wrap_format!( "We were unable to find gleam.toml. diff --git a/compiler-core/src/warning.rs b/compiler-core/src/warning.rs index dcd3134a875..0940146da62 100644 --- a/compiler-core/src/warning.rs +++ b/compiler-core/src/warning.rs @@ -167,6 +167,11 @@ pub enum Warning { DeprecatedEnvironmentVariable { variable: DeprecatedEnvironmentVariable, }, + + EmptyModule { + path: Utf8PathBuf, + name: EcoString, + }, } #[derive(Debug, Clone, Eq, PartialEq, Copy)] @@ -1390,6 +1395,14 @@ The imported value could not be used in this module anyway." location: None, } } + + Warning::EmptyModule { path: _, name } => Diagnostic { + title: "Empty module".into(), + text: format!("Module '{name}' contains no public definitions."), + hint: Some("Consider adding public functions, types, or constants, or removing this module.".into()), + level: diagnostic::Level::Warning, + location: None, + }, } } diff --git a/test-package-compiler/cases/empty_module_warning/gleam.toml b/test-package-compiler/cases/empty_module_warning/gleam.toml new file mode 100644 index 00000000000..ad53b41a8d1 --- /dev/null +++ b/test-package-compiler/cases/empty_module_warning/gleam.toml @@ -0,0 +1,7 @@ +name = "empty_module_warning" +version = "1.0.0" +description = "Test package with empty modules" +licences = ["Apache-2.0"] + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" \ No newline at end of file diff --git a/test-package-compiler/cases/empty_module_warning/src/empty.gleam b/test-package-compiler/cases/empty_module_warning/src/empty.gleam new file mode 100644 index 00000000000..01e2ce72c25 --- /dev/null +++ b/test-package-compiler/cases/empty_module_warning/src/empty.gleam @@ -0,0 +1 @@ +// This is an empty module \ No newline at end of file diff --git a/test-package-compiler/cases/empty_module_warning/src/main.gleam b/test-package-compiler/cases/empty_module_warning/src/main.gleam new file mode 100644 index 00000000000..5cfba492ac4 --- /dev/null +++ b/test-package-compiler/cases/empty_module_warning/src/main.gleam @@ -0,0 +1,3 @@ +pub fn main() { + "This module has public definitions" +} \ No newline at end of file diff --git a/test-package-compiler/src/generated_tests.rs b/test-package-compiler/src/generated_tests.rs index f46623bac8a..4f0353b79df 100644 --- a/test-package-compiler/src/generated_tests.rs +++ b/test-package-compiler/src/generated_tests.rs @@ -56,6 +56,17 @@ fn duplicate_module_test_dev() { ); } +#[rustfmt::skip] +#[test] +fn empty_module_warning() { + let output = crate::prepare("./cases/empty_module_warning"); + insta::assert_snapshot!( + "empty_module_warning", + output, + "./cases/empty_module_warning", + ); +} + #[rustfmt::skip] #[test] fn erlang_app_generation() { diff --git a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__empty_module_warning.snap b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__empty_module_warning.snap new file mode 100644 index 00000000000..5068f4bccd7 --- /dev/null +++ b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__empty_module_warning.snap @@ -0,0 +1,55 @@ +--- +source: test-package-compiler/src/generated_tests.rs +expression: "./cases/empty_module_warning" +snapshot_kind: text +--- +//// /out/lib/the_package/_gleam_artefacts/empty.cache +<.cache binary> + +//// /out/lib/the_package/_gleam_artefacts/empty.cache_inline +<8 byte binary> + +//// /out/lib/the_package/_gleam_artefacts/empty.cache_meta +<53 byte binary> + +//// /out/lib/the_package/_gleam_artefacts/empty.erl +-module(empty). + + +//// /out/lib/the_package/_gleam_artefacts/main.cache +<.cache binary> + +//// /out/lib/the_package/_gleam_artefacts/main.cache_inline +<8 byte binary> + +//// /out/lib/the_package/_gleam_artefacts/main.cache_meta +<61 byte binary> + +//// /out/lib/the_package/_gleam_artefacts/main.erl +-module(main). +-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). +-define(FILEPATH, "src/main.gleam"). +-export([main/0]). + +-file("src/main.gleam", 1). +-spec main() -> binary(). +main() -> + <<"This module has public definitions"/utf8>>. + + +//// /out/lib/the_package/ebin/empty_module_warning.app +{application, empty_module_warning, [ + {vsn, "1.0.0"}, + {applications, [gleam_stdlib]}, + {description, "Test package with empty modules"}, + {modules, [empty, + main]}, + {registered, []} +]}. + + +//// Warning +warning: Empty module + +Module 'empty' contains no public definitions. +Hint: Consider adding public functions, types, or constants, or removing this module. diff --git a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__erlang_app_generation.snap b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__erlang_app_generation.snap index 82c86833632..68ca1c0060f 100644 --- a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__erlang_app_generation.snap +++ b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__erlang_app_generation.snap @@ -1,6 +1,7 @@ --- source: test-package-compiler/src/generated_tests.rs expression: "./cases/erlang_app_generation" +snapshot_kind: text --- //// /out/lib/the_package/_gleam_artefacts/main.cache <.cache binary> @@ -29,3 +30,10 @@ expression: "./cases/erlang_app_generation" {modules, [main]}, {registered, []} ]}. + + +//// Warning +warning: Empty module + +Module 'main' contains no public definitions. +Hint: Consider adding public functions, types, or constants, or removing this module. diff --git a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__erlang_empty.snap b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__erlang_empty.snap index 8dd8431b0d2..310be66f71a 100644 --- a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__erlang_empty.snap +++ b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__erlang_empty.snap @@ -1,6 +1,7 @@ --- source: test-package-compiler/src/generated_tests.rs expression: "./cases/erlang_empty" +snapshot_kind: text --- //// /out/lib/the_package/_gleam_artefacts/empty.cache <.cache binary> @@ -23,3 +24,10 @@ expression: "./cases/erlang_empty" {modules, [empty]}, {registered, []} ]}. + + +//// Warning +warning: Empty module + +Module 'empty' contains no public definitions. +Hint: Consider adding public functions, types, or constants, or removing this module. diff --git a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__javascript_empty.snap b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__javascript_empty.snap index fc8ba8ed8c7..2eb2d7db0cb 100644 --- a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__javascript_empty.snap +++ b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__javascript_empty.snap @@ -1,6 +1,7 @@ --- source: test-package-compiler/src/generated_tests.rs expression: "./cases/javascript_empty" +snapshot_kind: text --- //// /out/lib/the_package/_gleam_artefacts/empty.cache <.cache binary> @@ -17,3 +18,10 @@ export {} //// /out/lib/the_package/gleam.mjs export * from "../prelude.mjs"; + + +//// Warning +warning: Empty module + +Module 'empty' contains no public definitions. +Hint: Consider adding public functions, types, or constants, or removing this module. diff --git a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__not_overwriting_erlang_module.snap b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__not_overwriting_erlang_module.snap index 89aeee5b533..1ab3fa259bd 100644 --- a/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__not_overwriting_erlang_module.snap +++ b/test-package-compiler/src/snapshots/test_package_compiler__generated_tests__not_overwriting_erlang_module.snap @@ -1,6 +1,7 @@ --- source: test-package-compiler/src/generated_tests.rs expression: "./cases/not_overwriting_erlang_module" +snapshot_kind: text --- //// /out/lib/the_package/_gleam_artefacts/app@code.cache <.cache binary> @@ -23,3 +24,10 @@ expression: "./cases/not_overwriting_erlang_module" {modules, [app@code]}, {registered, []} ]}. + + +//// Warning +warning: Empty module + +Module 'app/code' contains no public definitions. +Hint: Consider adding public functions, types, or constants, or removing this module.