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

Generate empty interfaces for executables and tests #3768

Merged
merged 6 commits into from
Feb 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Unreleased

- Add `(glob_files_rec <dir>/<glob>)` for globbing files recursively (#4176, @jeremiedimino)

- Automatically generate empty `.mli` files for executables and tests (#3768,
fixes #3745, @CraigFe)

2.8.2 (21/01/2021)
------------------

Expand Down
24 changes: 24 additions & 0 deletions doc/dune-files.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,24 @@ Starting from dune 2.0, dune mangles compilation units of executables by
default. However, this can still be turned off using ``(wrapped_executables
false)``

.. _executables_implicit_empty_intf:

executables_implicit_empty_intf
-------------------------------

By default, executables defined via ``(executables(s) ...)`` or ``(test(s)
...)`` stanzas are compiled with the interface file provided (e.g. ``.mli`` or
``rei``). Since these modules cannot be used as library dependencies, it's
common to give them empty interface files to strengthen the compiler's ability
to detect unused values in these modules.

Starting from dune 2.9, an option is available to automatically generate empty
interface files for executables and tests that don't already have them:

.. code:: scheme

(executables_implicit_empty_intf true)

.. _explicit-js-mode:

explicit_js_mode
Expand Down Expand Up @@ -633,6 +651,9 @@ binary at the same place as where ``ocamlc`` was found.
Executables can also be linked as object or shared object files. See
`linking modes`_ for more information.

Starting from dune 2.9, it's possible to automatically generate empty interface
files for executables. See `executables_implicit_empty_intf`_.

``<optional-fields>`` are:

- ``(public_name <public-name>)`` specifies that the executable should be
Expand Down Expand Up @@ -1344,6 +1365,9 @@ running dune runtest you can use the following stanza:
(libraries alcotest mylib)
(action (run %{test} -e)))

Starting from dune 2.9, it's possible to automatically generate empty interface
files for test executables. See `executables_implicit_empty_intf`_.

test
----

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
open Dune_action_plugin.V1
module Glob = Dune_glob.V1

let contains equal list elem = List.find list (equal elem) |> Option.is_some

let action =
let open Dune_action_plugin.V1.O in
let+ _ =
Expand Down
20 changes: 20 additions & 0 deletions src/dune_engine/dune_project.ml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ type t =
; parsing_context : Univ_map.t
; implicit_transitive_deps : bool
; wrapped_executables : bool
; executables_implicit_empty_intf : bool
; dune_version : Dune_lang.Syntax.Version.t
; generate_opam_files : bool
; use_standard_c_and_cxx_flags : bool option
Expand Down Expand Up @@ -210,6 +211,7 @@ let to_dyn
; packages
; implicit_transitive_deps
; wrapped_executables
; executables_implicit_empty_intf
; dune_version
; generate_opam_files
; use_standard_c_and_cxx_flags
Expand All @@ -232,6 +234,7 @@ let to_dyn
(Package.Name.Map.to_list packages) )
; ("implicit_transitive_deps", bool implicit_transitive_deps)
; ("wrapped_executables", bool wrapped_executables)
; ("executables_implicit_empty_intf", bool executables_implicit_empty_intf)
; ("dune_version", Dune_lang.Syntax.Version.to_dyn dune_version)
; ("generate_opam_files", bool generate_opam_files)
; ("use_standard_c_and_cxx_flags", option bool use_standard_c_and_cxx_flags)
Expand Down Expand Up @@ -550,6 +553,9 @@ let implicit_transitive_deps_default ~lang:_ = true
let wrapped_executables_default ~(lang : Lang.Instance.t) =
lang.version >= (2, 0)

let executables_implicit_empty_intf_default ~(lang : Lang.Instance.t) =
lang.version >= (3, 0)

let strict_package_deps_default ~(lang : Lang.Instance.t) =
lang.version >= (3, 0)

Expand Down Expand Up @@ -593,6 +599,9 @@ let infer ~dir packages =
in
let implicit_transitive_deps = implicit_transitive_deps_default ~lang in
let wrapped_executables = wrapped_executables_default ~lang in
let executables_implicit_empty_intf =
executables_implicit_empty_intf_default ~lang
in
let explicit_js_mode = explicit_js_mode_default ~lang in
let strict_package_deps = strict_package_deps_default ~lang in
let root = dir in
Expand All @@ -604,6 +613,7 @@ let infer ~dir packages =
; version = None
; implicit_transitive_deps
; wrapped_executables
; executables_implicit_empty_intf
; stanza_parser
; project_file
; extension_args
Expand Down Expand Up @@ -695,6 +705,9 @@ let parse ~dir ~lang ~opam_packages ~file ~dir_status =
"It is useless since the Merlin configurations are not ambiguous \
anymore."
loc lang.syntax (2, 8) ~what:"This field"
and+ executables_implicit_empty_intf =
field_o_b "executables_implicit_empty_intf"
~check:(Dune_lang.Syntax.since Stanza.syntax (2, 9))
and+ () = Dune_lang.Versioned_file.no_more_lang
and+ generate_opam_files =
field_o_b "generate_opam_files"
Expand Down Expand Up @@ -805,6 +818,10 @@ let parse ~dir ~lang ~opam_packages ~file ~dir_status =
Option.value wrapped_executables
~default:(wrapped_executables_default ~lang)
in
let executables_implicit_empty_intf =
Option.value executables_implicit_empty_intf
~default:(executables_implicit_empty_intf_default ~lang)
in
let strict_package_deps =
Option.value strict_package_deps
~default:(strict_package_deps_default ~lang)
Expand Down Expand Up @@ -864,6 +881,7 @@ let parse ~dir ~lang ~opam_packages ~file ~dir_status =
; parsing_context
; implicit_transitive_deps
; wrapped_executables
; executables_implicit_empty_intf
; dune_version
; generate_opam_files
; use_standard_c_and_cxx_flags
Expand Down Expand Up @@ -913,6 +931,8 @@ let set_parsing_context t parser =

let wrapped_executables t = t.wrapped_executables

let executables_implicit_empty_intf t = t.executables_implicit_empty_intf

let () =
let open Dune_lang.Decoder in
Extension.register_simple Action_plugin.syntax (return []);
Expand Down
2 changes: 2 additions & 0 deletions src/dune_engine/dune_project.mli
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ val dune_version : t -> Dune_lang.Syntax.Version.t

val wrapped_executables : t -> bool

val executables_implicit_empty_intf : t -> bool

val strict_package_deps : t -> bool

val cram : t -> bool
Expand Down
29 changes: 28 additions & 1 deletion src/dune_rules/exe_rules.ml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,20 @@ let o_files sctx ~dir ~expander ~(exes : Executables.t) ~linkages ~dir_contents
~dir_contents ~foreign_sources
|> List.map ~f:Path.build

let with_empty_intf ~sctx ~dir module_ =
let name =
Module.file module_ ~ml_kind:Impl
|> Option.value_exn
|> Path.set_extension ~ext:".mli"
in
let rule =
Action_builder.write_file
(Path.as_in_build_dir_exn name)
"(* Auto-generated by Dune *)"
in
Super_context.add_rule sctx ~dir rule;
Module.add_file module_ Ml_kind.Intf (Module.File.make Dialect.ocaml name)

let executables_rules ~sctx ~dir ~expander ~dir_contents ~scope ~compile_info
~embed_in_plugin_libraries (exes : Dune_file.Executables.t) =
(* Use "eobjs" rather than "objs" to avoid a potential conflict with a library
Expand All @@ -110,9 +124,22 @@ let executables_rules ~sctx ~dir ~expander ~dir_contents ~scope ~compile_info
~lint:exes.buildable.lint ~lib_name:None
in
let modules =
let executable_names =
List.map exes.names ~f:Module_name.of_string_allow_invalid
in
Modules.map_user_written modules ~f:(fun m ->
let name = Module.name m in
Pp_spec.pp_module_as pp name m)
let m = Pp_spec.pp_module_as pp name m in
let add_empty_intf =
let project = Scope.project scope in
Dune_project.executables_implicit_empty_intf project
&& List.mem name ~set:executable_names
&& not (Module.has m ~ml_kind:Intf)
in
if add_empty_intf then
with_empty_intf ~sctx ~dir m
else
m)
in
let programs = programs ~modules ~exes in
let explicit_js_mode = Dune_project.explicit_js_mode (Scope.project scope) in
Expand Down
12 changes: 12 additions & 0 deletions src/dune_rules/module.ml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,14 @@ module Source = struct
| None, Some x ->
x

let add_file t ml_kind file =
if has t ~ml_kind then
Code_error.raise "Attempted to add a duplicate file to module"
[ ("module", to_dyn t); ("file", File.to_dyn file) ];
match ml_kind with
| Ml_kind.Impl -> { t with files = { t.files with impl = Some file } }
| Intf -> { t with files = { t.files with intf = Some file } }

let src_dir t = Path.parent_exn (choose_file t).path

let map_files t ~f =
Expand Down Expand Up @@ -179,6 +187,10 @@ let iter t ~f =
let with_wrapper t ~main_module_name =
{ t with obj_name = Module_name.wrap t.source.name ~with_:main_module_name }

let add_file t kind file =
let source = Source.add_file t.source kind file in
{ t with source }

let map_files t ~f =
let source =
Source.map_files t.source ~f:(fun kind -> Option.map ~f:(f kind))
Expand Down
2 changes: 2 additions & 0 deletions src/dune_rules/module.mli
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ val has : t -> ml_kind:Ml_kind.t -> bool
(** Prefix the object name with the library name. *)
val with_wrapper : t -> main_module_name:Module_name.t -> t

val add_file : t -> Ml_kind.t -> File.t -> t
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fairly arbitrary interface choice here as it ended up being concise at the call-site; alternatives welcome.


val map_files : t -> f:(Ml_kind.t -> File.t -> File.t) -> t

(** Set preprocessing flags *)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let a = 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
(executable
(name executable))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let unused = Dependency.a
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
(executable
(name executable))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let a = 1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
val a : int
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Executables with no corresponding `.mli` file will have one generated for them
by Dune:

$ cat >dune-project <<EOF
> (lang dune 2.9)
> (executables_implicit_empty_intf true)
> EOF

$ dune build ./bin/executable.exe
File "bin/executable.ml", line 1, characters 4-10:
1 | let unused = Dependency.a
^^^^^^
Error (warning 32): unused value unused.
[1]

$ test ! -f _build/default/bin/dependency.mli
$ cat _build/default/bin/executable.mli
(* Auto-generated by Dune *)

as will test binaries:

$ dune runtest
File "test/test.ml", line 1, characters 4-10:
1 | let unused = 1
^^^^^^
Error (warning 32): unused value unused.
[1]

$ cat _build/default/test/test.mli
(* Auto-generated by Dune *)

If an executable already has an interface, it is preserved:

$ dune clean
$ dune build ./bin_with_intf/executable.exe
$ cat _build/default/bin_with_intf/executable.mli
val a : int

Generation of empty `.mli` files is disabled by default prior to lang 3.0:

$ dune clean
$ echo >dune-project "(lang dune 2.9)"
$ dune build ./bin/executable.exe
$ dune runtest
$ test ! -f _build/default/bin/executable.mli
$ test ! -f _build/default/test/test.mli

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
(test
(name test))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let unused = 1