Skip to content

Commit 3a2285b

Browse files
feat: add empty module validation
Prevent publishing packages with empty modules and emit warnings during compilation when modules contain no public definitions.
1 parent a7fc4ad commit 3a2285b

14 files changed

+210
-0
lines changed

compiler-cli/src/publish.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,27 @@ fn do_build_hex_tarball(paths: &ProjectPaths, config: &mut PackageConfig) -> Res
382382
});
383383
}
384384

385+
let mut empty_modules = vec![];
386+
387+
for module in built.root_package.modules.iter() {
388+
let public_definitions = module
389+
.ast
390+
.definitions
391+
.iter()
392+
.filter(|def| def.is_public())
393+
.count();
394+
395+
if public_definitions == 0 {
396+
empty_modules.push(module.name.clone());
397+
}
398+
}
399+
400+
if !empty_modules.is_empty() {
401+
return Err(Error::CannotPublishEmptyModules {
402+
unfinished: empty_modules,
403+
});
404+
}
405+
385406
// TODO: If any of the modules in the package contain a leaked internal type then
386407
// refuse to publish as the package is not yet finished.
387408
// We need to move aliases in to the type system first.

compiler-core/src/ast.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,16 @@ impl TypedDefinition {
868868
}
869869
}
870870

871+
pub fn is_public(&self) -> bool {
872+
match self {
873+
Definition::Function(f) => f.publicity.is_public(),
874+
Definition::TypeAlias(t) => t.publicity.is_public(),
875+
Definition::CustomType(t) => t.publicity.is_public(),
876+
Definition::ModuleConstant(c) => c.publicity.is_public(),
877+
Definition::Import(_) => false,
878+
}
879+
}
880+
871881
pub fn find_node(&self, byte_index: u32) -> Option<Located<'_>> {
872882
match self {
873883
Definition::Function(function) => {

compiler-core/src/build/package_compiler.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,19 @@ fn analyse(
550550
// Register the types from this module so they can be imported into
551551
// other modules.
552552
let _ = module_types.insert(module.name.clone(), module.ast.type_info.clone());
553+
554+
// Check for empty modules and emit warning
555+
let public_definitions = module.ast.definitions.iter()
556+
.filter(|def| def.is_public())
557+
.count();
558+
559+
if public_definitions == 0 {
560+
warnings.emit(crate::warning::Warning::EmptyModule {
561+
path: module.input_path.clone(),
562+
name: module.name.clone(),
563+
});
564+
}
565+
553566
// Register the successfully type checked module data so that it can be
554567
// used for code generation and in the language server.
555568
modules.push(module);

compiler-core/src/error.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,9 @@ file_names.iter().map(|x| x.as_str()).join(", "))]
298298
)]
299299
CannotPublishLeakedInternalType { unfinished: Vec<EcoString> },
300300

301+
#[error("The modules {unfinished:?} are empty and so cannot be published")]
302+
CannotPublishEmptyModules { unfinished: Vec<EcoString> },
303+
301304
#[error("Publishing packages to reserve names is not permitted")]
302305
HexPackageSquatting,
303306

@@ -1046,6 +1049,25 @@ Please make sure internal types do not appear in public functions and try again.
10461049
location: None,
10471050
}],
10481051

1052+
Error::CannotPublishEmptyModules { unfinished } => vec![Diagnostic {
1053+
title: "Cannot publish empty modules".into(),
1054+
text: format!(
1055+
"These modules contain no public definitions and cannot be published:
1056+
1057+
{}
1058+
1059+
Please add public functions, types, or constants to these modules, or remove them and try again.
1060+
",
1061+
unfinished
1062+
.iter()
1063+
.map(|name| format!(" - {}", name.as_str()))
1064+
.join("\n")
1065+
),
1066+
level: Level::Error,
1067+
hint: None,
1068+
location: None,
1069+
}],
1070+
10491071
Error::UnableToFindProjectRoot { path } => {
10501072
let text = wrap_format!(
10511073
"We were unable to find gleam.toml.

compiler-core/src/warning.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ pub enum Warning {
167167
DeprecatedEnvironmentVariable {
168168
variable: DeprecatedEnvironmentVariable,
169169
},
170+
171+
EmptyModule {
172+
path: Utf8PathBuf,
173+
name: EcoString,
174+
},
170175
}
171176

172177
#[derive(Debug, Clone, Eq, PartialEq, Copy)]
@@ -1371,6 +1376,22 @@ The imported value could not be used in this module anyway."
13711376
location: None,
13721377
}
13731378
}
1379+
1380+
Warning::EmptyModule { path, name } => Diagnostic {
1381+
title: "Empty module".into(),
1382+
text: format!("Module '{}' contains no public definitions.", name),
1383+
hint: Some("Consider adding public functions, types, or constants, or removing this module.".into()),
1384+
level: diagnostic::Level::Warning,
1385+
location: Some(Location {
1386+
label: diagnostic::Label {
1387+
text: Some("This module is empty".into()),
1388+
span: SrcSpan { start: 0, end: 0 },
1389+
},
1390+
path: path.clone(),
1391+
src: EcoString::from(""),
1392+
extra_labels: vec![],
1393+
}),
1394+
},
13741395
}
13751396
}
13761397

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
name = "empty_module_publish_validation"
2+
version = "1.0.0"
3+
description = "Test package with empty modules"
4+
licences = ["Apache-2.0"]
5+
6+
[dependencies]
7+
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// This is an empty module
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub fn main() {
2+
"This module has public definitions"
3+
}

test-package-compiler/src/generated_tests.rs

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
source: test-package-compiler/src/generated_tests.rs
3+
expression: "./cases/empty_module_publish_validation"
4+
snapshot_kind: text
5+
---
6+
//// /out/lib/the_package/_gleam_artefacts/empty.cache
7+
<.cache binary>
8+
9+
//// /out/lib/the_package/_gleam_artefacts/empty.cache_meta
10+
<53 byte binary>
11+
12+
//// /out/lib/the_package/_gleam_artefacts/empty.erl
13+
-module(empty).
14+
15+
16+
//// /out/lib/the_package/_gleam_artefacts/main.cache
17+
<.cache binary>
18+
19+
//// /out/lib/the_package/_gleam_artefacts/main.cache_meta
20+
<61 byte binary>
21+
22+
//// /out/lib/the_package/_gleam_artefacts/main.erl
23+
-module(main).
24+
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
25+
-define(FILEPATH, "src/main.gleam").
26+
-export([main/0]).
27+
28+
-file("src/main.gleam", 1).
29+
-spec main() -> binary().
30+
main() ->
31+
<<"This module has public definitions"/utf8>>.
32+
33+
34+
//// /out/lib/the_package/ebin/empty_module_publish_validation.app
35+
{application, empty_module_publish_validation, [
36+
{vsn, "1.0.0"},
37+
{applications, [gleam_stdlib]},
38+
{description, "Test package with empty modules"},
39+
{modules, [empty,
40+
main]},
41+
{registered, []}
42+
]}.
43+
44+
45+
//// Warning
46+
warning: Empty module
47+
┌─ src/empty.gleam:1:1
48+
49+
1
50+
^ This module is empty
51+
52+
Module 'empty' contains no public definitions.
53+
Hint: Consider adding public functions, types, or constants, or removing this module.

0 commit comments

Comments
 (0)