I was writing tests for my code and ran into some strange compiler errors. In the end, a simple file rename took all my problems away, but after the fact I decided to take a closer look. Investigation results follow. I think I’m onto something.
I’m a bit unsure if this is something that would need fixing or not, and whether the fix would be an addition to documentation, better error messages, or a change somewhere in the compiler.
Setup
Consider a project structure like the following:
example
|---bsconfig.json
|---package.json
\---src
|---Command.res
|---Example.res
\---Library.res
Example.res
and Library.res
are stand-alone, library type files, while Command.res
uses types and values from the other modules. Code works. So far so good!
As a minimal example the Example.res
and Library.res
files could contain the following:
type t = string
let a: t = "example string"
And Command.res
could contain the following:
let exampleValue = Example.a
type exampleType = Example.t
let libraryValue = Library.a
type libraryType = Library.t
Now, imagine the author wants to write some unit tests documenting module usage. One common naming convention would call for a file called Example.test.res
. This is an exotic module filename and the documentation warns that it would not be accessible from other modules. Sounds like an acceptable tradeoff from sticking with a familiar naming scheme, doesn’t it?
How it works out depends on bsconfig.json
values.
Scenario 1
-
namespace
isfalse
Result:
-
Example.test.res
cannot access values or types fromExample.res
: The module or file Example can’t be found. -
Example.test.res
can accessLibrary.res
andCommand.res
.
Demo:
Example.test.res
/* Scenario 1 - namespace: false */
/* let exampleValue = Example.a // The module or file Example can't be found. */
/* type exampleType = Example.t // The module or file Example can't be found. */
/* let libraryValue = Library.a // OK */
/* type libraryType = Library.t // OK */
/* let commandExampleValue = Command.exampleValue // OK */
/* type commandExampleType = Command.exampleType // OK */
/* let commandLibraryValue = Command.libraryValue // OK */
/* type commandLibraryType = Command.libraryType // OK */
Scenario 2
-
namespace
istrue
-
name
ismy-example
(or something else but notexample
– and not matching any other ReScript modules just to be extra sure)
Result:
- Everything works as expected.
Demo:
Example.test.res
/* Scenario 2 - namespace: true, name: my-example */
/* let exampleValue = Example.a // OK */
/* type exampleType = Example.t // OK */
/* let libraryValue = Library.a // OK */
/* type libraryType = Library.t // OK */
/* let commandExampleValue = Command.exampleValue // OK */
/* type commandExampleType = Command.exampleType // OK */
/* let commandLibraryValue = Command.libraryValue // OK */
/* type commandLibraryType = Command.libraryType // OK */
Scenario 3
-
namespace
istrue
-
name
isexample
Result:
-
Example.test.res
cannot access values from any of the other modules: Internal path Example.XXX is dangling. -
Example.test.res
cannot access types from any of the other modules: Fatal error: exception Not_found
Demo:
Example.test.res
/* Scenario 3 - namespace: true, name: example */
/* let exampleValue = Example.a // Internal path Example.Example is dangling. */
/* type libraryType = Library.t // Fatal error: exception Not_found */
/* let libraryValue = Library.a // Internal path Example.Library is dangling. */
/* type exampleType = Example.t // Fatal error: exception Not_found */
/* let commandExampleValue = Command.exampleValue // Internal path Example.Command is dangling. */
/* type commandExampleType = Command.exampleType // Fatal error: exception Not_found */
/* let commandLibraryValue = Command.libraryValue // Internal path Example.Command is dangling */
/* type commandLibraryType = Command.libraryType // Fatal error: exception Not_found */
Questions and discussion
First and foremost, shouldn’t namespace settings be completely irrelevant to any of this? The build configuration documentation says in bold letters: “The namespacing affects your consumers, not yourself.” I think I’ve found a case where namespacing most definitely affects me.
Should a module with an exotic filename (e.g., one with .test.
affix) be able to access a module without the extra part in the middle? If this is not a supported use case, does it work by mere luck in scenario 2? Could there be a clear warning against such use here: https://rescript-lang.org/docs/manual/latest/module#exotic-module-filenames? For writing tests, the documentation might recommend using a non-exotic filename such as ExampleTest.res
instead. That kind of small documentation change would be a very low-hanging fruit.
I would have expected the module with the exotic filename to be able to access other, unrelated modules in scenario 3 even if the non-exotic module with the same base name was acting up. And… what exactly is wrong here? The error messages are hard to understand. What on earth is a “dangling path” and can I, as a user, do something about it? Also, why does accessing a type result in a different error that looks like quite a hard crash? If this is something that is not supported or otherwise OK to fail, a more detailed error message would make for a better developer experience.
Also comes to mind, is scenario 3 a rare corner case related to exotic file names only, or could the combination of matching namespace and module names be problematic in other ways, too?
To wrap it up, my acute problem was easy to work around by simply changing the name of the file, and other workarounds exist. Still, there seems to be a growing interest in exotic module filenames, and I think having things like this as polished as possible is gaining relevance.