You are including two copies of ModuleB when one would be enough, e.g. like so:
module ModuleA
include("ModuleB.jl")
export process
# change: we want to re-export `Foo`
export Foo
using .ModuleB
function process(foo::Foo)
end
end
(ModuleB.jl stays the same)
include("ModuleA.jl")
# change: no include or using needed
using .ModuleA
process(Foo())
What happens is that there are basically two versions of Foo in your original code (note that include just pastes the file content at that position). One is Main.ModuleA.ModuleB.Foo and one is Main.ModuleB.Foo which exists, because there is another module B in your ModuleC.jl from the second include("ModuleB.jl").
Since there is an export Foo in ModuleB but not from ModuleA, whenever you use the name Foo in the main Module, it will be Main.ModuleB.Foo. If you try to add export Foo just to ModuleA.jl without removing the second include and using .ModuleB, you will also get a warning about two objects with the same name – so you cannot use Foo in this case at all.
The function in ModuleA uses Foo from the “inner” ModuleB, i.e. Main.ModuleA.ModuleB.Foo hence you get a method error if you try to call it with Foo from the top-level module.
In principle, the two structs in the two copies of ModuleB are different things that could also have differend code and just happen to have the same names. You only need one copy of the code of ModuleB (i.e. only one include) and then you can propagate the names as you like.
Another option would be to also export the whole ModuleB from ModuleA
module ModuleA
include("ModuleB.jl")
export process
export ModuleB
using .ModuleB
function process(foo::Foo)
end
end
Then you can do
include("ModuleA.jl")
# just remove the second `include`
using .ModuleA
using .ModuleB
process(Foo())
If there are many things from ModuleB you want to pass on through ModuleA, perhaps Reexport.jl is also useful.