Opinionated OCaml linter with Merlin integration for code quality, naming conventions, and style checks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

merlint(E610): handle sublib subdirs and module-alias references

Two complementary fixes so a [test/<sub>/test_<mod>.ml] for a leaf
sublib (no internal callers besides a [module X = <Mod>] alias) is
no longer falsely flagged as missing.

- Path matching: the basename branch compared
[Filename.basename lib_path] to the full [expected_path], which
only fired when the test sat directly under [test/]. Replace with
a symmetric basename match plus a sublib-prefix check, so a
library file at [lib/<sub>/<dir>/<mod>.ml] now satisfies a test
at [test/<sub>/test_<mod>.ml].

- Reference detection: extend [is_referenced_in_library] to accept
the canonical alias forms [= <Mod>] and [: <Mod>] alongside the
existing [<Mod>.] and [module <Mod>] patterns. The wrapper
pattern [module Cmd = Xrpc_auth_cmd] now counts as a reference
to [Xrpc_auth_cmd], which it materially is.

Existing cram tests still pass.

+34 -5
+34 -5
lib/rules/e610.ml
··· 81 81 in 82 82 83 83 (* Check if [module_name] (e.g. "Dump") is referenced as a module in any 84 - library source file. Looks for patterns like [Foo.Dump.] or 85 - [module Dump]. *) 84 + library source file. Looks for the three textual forms a library can take 85 + to use the module: 86 + - [Foo.Dump.…] / [Dump.…] — direct member access 87 + - [module Dump = …] — defines it 88 + - [… = Dump] / [… : Dump] — re-exports it via [module X = Dump] or 89 + [module X : module type of Dump]; the 90 + wrapper-alias case where [Dump] is the 91 + RHS, never appearing with a trailing 92 + dot. *) 86 93 let is_referenced_in_library module_name = 87 94 let cap_name = String.capitalize_ascii module_name in 88 95 let pattern_dot = cap_name ^ "." in 89 96 let pattern_module = "module " ^ cap_name in 97 + let pattern_eq = "= " ^ cap_name in 98 + let pattern_colon = ": " ^ cap_name in 90 99 List.exists 91 100 (fun src_path -> 92 101 try ··· 95 104 in 96 105 Astring.String.is_infix ~affix:pattern_dot content 97 106 || Astring.String.is_infix ~affix:pattern_module content 107 + || Astring.String.is_infix ~affix:pattern_eq content 108 + || Astring.String.is_infix ~affix:pattern_colon content 98 109 with _ -> false) 99 110 library_source_files 100 111 in ··· 123 134 expected_path); 124 135 let found = 125 136 let expected_lc = String.lowercase_ascii expected_path in 137 + let expected_dir = 138 + String.lowercase_ascii (Filename.dirname expected_path) 139 + in 140 + let expected_base = 141 + String.lowercase_ascii (Filename.basename expected_path) 142 + in 126 143 List.exists 127 144 (fun lib_path -> 128 - String.lowercase_ascii lib_path = expected_lc 129 - || String.lowercase_ascii (Filename.basename lib_path) 130 - = expected_lc) 145 + let lib_lc = String.lowercase_ascii lib_path in 146 + lib_lc = expected_lc 147 + || 148 + (* Same module basename and same sublib bucket: 149 + a sublib subdir like [lib/<sub>/<dir>/<mod>.ml] is 150 + an acceptable home for a test at 151 + [test/<sub>/test_<mod>.ml]. *) 152 + let lib_base = 153 + String.lowercase_ascii (Filename.basename lib_path) 154 + in 155 + lib_base = expected_base 156 + && (expected_dir = "." 157 + || String.starts_with ~prefix:(expected_dir ^ "/") 158 + lib_lc 159 + || (expected_dir = "" && lib_lc = lib_base))) 131 160 library_module_paths 132 161 in 133 162 (* Also check if the expected module name is referenced as a