diff --git a/haskell/defs.bzl b/haskell/defs.bzl index 6afba9a06..fde4090b6 100644 --- a/haskell/defs.bzl +++ b/haskell/defs.bzl @@ -615,7 +615,7 @@ def haskell_library( non_default_plugins: Like `plugins` but doesn't pass `-fplugin=...` to modules by default. tools: Extra tools needed at compile-time, like preprocessors. worker: Experimental. Worker binary employed by Bazel's persistent worker mode. See [use-cases documentation](https://rules-haskell.readthedocs.io/en/latest/haskell-use-cases.html#persistent-worker-mode-experimental). - hidden_modules: Modules that should be unavailable for import by dependencies. + hidden_modules: Modules that should be unavailable for import by any Haskell module outside of this library. reexported_modules: A dictionary mapping dependencies to module reexports that should be available for import by dependencies. exports: A list of other haskell libraries that will be transparently added as a dependency to every downstream rule linkstatic: Create a static library, not both a static and a shared library. diff --git a/haskell/private/haskell_impl.bzl b/haskell/private/haskell_impl.bzl index 3840a71ec..e7f881571 100644 --- a/haskell/private/haskell_impl.bzl +++ b/haskell/private/haskell_impl.bzl @@ -526,7 +526,15 @@ def haskell_library_impl(ctx): other_modules = ctx.attr.hidden_modules exposed_modules_reexports = _exposed_modules_reexports(ctx.attr.reexported_modules) + haskell_module_names = [haskell_module_from_target(m) for m in modules] + + # Validate that hidden modules appear as modules in src list or modules list, depending which appears: + declared_modules = haskell_module_names if modules else module_map.keys() + hidden_minus_declared_modules = set.difference(set.from_list(ctx.attr.hidden_modules), set.from_list(declared_modules)) + if not hidden_minus_declared_modules == set.empty(): + fail("""Hidden modules must be a subset of all modules, found additional hidden modules {}""".format(set.to_list(hidden_minus_declared_modules))) + exposed_modules = set.from_list(module_map.keys() + exposed_modules_reexports + haskell_module_names) set.mutable_difference(exposed_modules, set.from_list(other_modules)) exposed_modules = set.to_list(exposed_modules) diff --git a/haskell/private/set.bzl b/haskell/private/set.bzl index 66fbd5723..75430b157 100644 --- a/haskell/private/set.bzl +++ b/haskell/private/set.bzl @@ -89,6 +89,22 @@ def _mutable_union(s0, s1): s0._set_items.update(s1._set_items) return s0 +def _difference(s0, s1): + """Return the set of elements from s0 not appearing in s1. + + Args: + s0: One set. + s1: Another set. + + Result: + set, difference of the two sets. + """ + s2 = _empty() + for item in s0._set_items.keys(): + if not _is_member(s1, item): + _mutable_insert(s2, item) + return s2 + def _mutable_difference(s0, s1): """Modify set `s0` removing elements from `s1` from it. @@ -157,6 +173,7 @@ set = struct( mutable_insert = _mutable_insert, union = _union, mutable_union = _mutable_union, + difference = _difference, mutable_difference = _mutable_difference, map = _map, from_list = _from_list, diff --git a/tests/hidden-modules/BUILD.bazel b/tests/hidden-modules/BUILD.bazel index c87c3386b..e26df7343 100644 --- a/tests/hidden-modules/BUILD.bazel +++ b/tests/hidden-modules/BUILD.bazel @@ -2,6 +2,10 @@ load( "@rules_haskell//haskell:defs.bzl", "haskell_library", ) +load( + "hidden_modules_test.bzl", + "hidden_modules_test_suite", +) package( default_testonly = 1, @@ -34,6 +38,10 @@ haskell_library( ], ) +hidden_modules_test_suite( + name = "test-missing-module", +) + filegroup( name = "all_files", testonly = True, diff --git a/tests/hidden-modules/hidden_modules_test.bzl b/tests/hidden-modules/hidden_modules_test.bzl new file mode 100644 index 000000000..9399a88e3 --- /dev/null +++ b/tests/hidden-modules/hidden_modules_test.bzl @@ -0,0 +1,79 @@ +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load("@rules_haskell//haskell:defs.bzl", "haskell_library") +load("@rules_haskell//haskell/experimental:defs.bzl", "haskell_module") + +# Test for failure in the case of hiding non existent modules +def _hidden_module_test_impl(ctx): + env = analysistest.begin(ctx) + + asserts.expect_failure(env, "Hidden modules must be a subset of all modules") + + return analysistest.end(env) + +hidden_module_test = analysistest.make( + _hidden_module_test_impl, + expect_failure = True, +) + +# test using the standard haskell library rules +def _test_hidden_modules1(): + haskell_library( + name = "lib-hidden-1", + srcs = native.glob(["lib-a/*.hs"]), + src_strip_prefix = "lib-a", + hidden_modules = ["NotHere"], + deps = ["//tests/hackage:base"], + tags = ["manual"], + ) + + hidden_module_test( + name = "hidden_module_test-1", + target_under_test = ":lib-hidden-1", + ) + +# Test using haskell modules +def _test_hidden_modules2(): + haskell_module( + name = "FooModule", + src = "lib-a/Foo.hs", + module_name = "Foo", + ) + + haskell_module( + name = "BarModule", + src = "lib-a/Bar.hs", + module_name = "Bar", + ) + + haskell_library( + name = "lib-hidden-2", + modules = [ + ":FooModule", + ":BarModule", + ], + # Should be [Bar] if you actually want to hide the Bar module + hidden_modules = ["BarModule"], + visibility = ["//visibility:public"], + deps = [ + "//tests/hackage:base", + ], + tags = ["manual"], + ) + + hidden_module_test( + name = "hidden_module_test-2", + target_under_test = ":lib-hidden-2", + ) + +# Run all of the expect failure tests +def hidden_modules_test_suite(name): + _test_hidden_modules1() + _test_hidden_modules2() + + native.test_suite( + name = name, + tests = [ + ":hidden_module_test-1", + ":hidden_module_test-2", + ], + )