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.

ocaml-merkle-sync: generic Merkle DAG sync; irmin Sync.S module type

ocaml-merkle-sync: reusable sync primitives for any content-addressed DAG.
- Layer 1: anti-entropy gossip (branch head exchange, O(log n))
- Layer 2: merkle descent (DAG diff by hash, dependency-order transfer)
- Bloom filter (FNV-1a, ~1% FP rate)
15 tests passing. Fuzz harness for bloom properties.

irmin/lib/sync.ml: Sync.S module type — discover/locate/fetch/push.
Each backend provides its own implementation.

Also: dune fmt across affected packages.

+119 -47
+4 -3
bin/main.ml
··· 237 237 Hashtbl.replace by_rule e.rule (prev + 1)) 238 238 all_excluded; 239 239 Fmt.pr "@[<v>%a %d issues suppressed by .merlintrc exclusions:@," 240 - Fmt.(styled `Yellow string) "!" n; 241 - Hashtbl.iter (fun rule count -> 242 - Fmt.pr " [%s] %d suppressed@," rule count) 240 + Fmt.(styled `Yellow string) 241 + "!" n; 242 + Hashtbl.iter 243 + (fun rule count -> Fmt.pr " [%s] %d suppressed@," rule count) 243 244 by_rule; 244 245 Fmt.pr "@]@." 245 246 end;
+113 -36
lib/engine.ml
··· 1 1 (** Linting engine *) 2 2 3 3 let src = Logs.Src.create "merlint.engine" ~doc:"Linting engine" 4 + 4 5 module Log = (val Logs.src_log src : Logs.LOG) 5 6 6 7 type exclusion_stats = { rule : string; file : string } ··· 10 11 let code = Rule.code rule in 11 12 Log.debug (fun m -> m "Running rule %s on %s" code ctx.Context.filename); 12 13 let start_time = Unix.gettimeofday () in 13 - let res = try Rule.Run.file rule ctx with exn -> 14 - Log.err (fun m -> m "Rule %s failed on %s: %s" code ctx.Context.filename (Printexc.to_string exn)); [] in 14 + let res = 15 + try Rule.Run.file rule ctx 16 + with exn -> 17 + Log.err (fun m -> 18 + m "Rule %s failed on %s: %s" code ctx.Context.filename 19 + (Printexc.to_string exn)); 20 + [] 21 + in 15 22 let duration = Unix.gettimeofday () -. start_time in 16 - (match profiling with Some prof -> Profiling.add_timing prof { operation = Profiling.File_rule { rule_code = code; filename = ctx.Context.filename }; duration } | None -> ()); res 23 + (match profiling with 24 + | Some prof -> 25 + Profiling.add_timing prof 26 + { 27 + operation = 28 + Profiling.File_rule 29 + { rule_code = code; filename = ctx.Context.filename }; 30 + duration; 31 + } 32 + | None -> ()); 33 + res 17 34 18 35 let run_project_rule ?profiling ctx rule = 19 36 let code = Rule.code rule in 20 37 Log.debug (fun m -> m "Running project rule %s" code); 21 38 let start_time = Unix.gettimeofday () in 22 - let res = try Rule.Run.project rule ctx with exn -> 23 - Log.err (fun m -> m "Project rule %s failed: %s" code (Printexc.to_string exn)); [] in 39 + let res = 40 + try Rule.Run.project rule ctx 41 + with exn -> 42 + Log.err (fun m -> 43 + m "Project rule %s failed: %s" code (Printexc.to_string exn)); 44 + [] 45 + in 24 46 let duration = Unix.gettimeofday () -. start_time in 25 - (match profiling with Some prof -> Profiling.add_timing prof { operation = Profiling.Project_rule code; duration } | None -> ()); res 47 + (match profiling with 48 + | Some prof -> 49 + Profiling.add_timing prof 50 + { operation = Profiling.Project_rule code; duration } 51 + | None -> ()); 52 + res 26 53 27 54 let setup_analysis ~filter ~dune_describe project_root = 28 55 let config = Config.load_from_path project_root in 29 56 let files_to_analyze = Dune.project_files dune_describe in 30 57 let files_to_analyze_str = List.map Fpath.to_string files_to_analyze in 31 - let project_ctx = Context.project ~config ~project_root ~all_files:files_to_analyze_str ~dune_describe in 32 - let enabled_rules = Data.all_rules |> List.filter (fun rule -> Filter.is_enabled_by_code filter (Rule.code rule)) in 58 + let project_ctx = 59 + Context.project ~config ~project_root ~all_files:files_to_analyze_str 60 + ~dune_describe 61 + in 62 + let enabled_rules = 63 + Data.all_rules 64 + |> List.filter (fun rule -> 65 + Filter.is_enabled_by_code filter (Rule.code rule)) 66 + in 33 67 (config, files_to_analyze, project_ctx, enabled_rules) 34 68 35 69 let run_project_rules ?profiling enabled_rules project_ctx = 36 70 let config = project_ctx.Context.config in 37 71 let excluded_acc = ref [] in 38 - let issues = enabled_rules |> List.filter Rule.is_project_scoped |> List.concat_map (fun rule -> 39 - let code = Rule.code rule in 40 - let issues = run_project_rule ?profiling project_ctx rule in 41 - List.filter (fun r -> match Rule.Run.location r with 42 - | Some loc -> let file = loc.Location.file in 43 - let skip = Rule_config.should_exclude config.exclusions ~rule:code ~file in 44 - if skip then excluded_acc := { rule = code; file } :: !excluded_acc; 45 - not skip 46 - | None -> true) issues) in 72 + let issues = 73 + enabled_rules 74 + |> List.filter Rule.is_project_scoped 75 + |> List.concat_map (fun rule -> 76 + let code = Rule.code rule in 77 + let issues = run_project_rule ?profiling project_ctx rule in 78 + List.filter 79 + (fun r -> 80 + match Rule.Run.location r with 81 + | Some loc -> 82 + let file = loc.Location.file in 83 + let skip = 84 + Rule_config.should_exclude config.exclusions ~rule:code ~file 85 + in 86 + if skip then 87 + excluded_acc := { rule = code; file } :: !excluded_acc; 88 + not skip 89 + | None -> true) 90 + issues) 91 + in 47 92 (issues, List.rev !excluded_acc) 48 93 49 - let analyze_single_file ?profiling ~backend ~config ~project_root ~file_rules filepath = 94 + let analyze_single_file ?profiling ~backend ~config ~project_root ~file_rules 95 + filepath = 50 96 let filename = Fpath.to_string filepath in 51 97 let excluded_acc = ref [] in 52 - let issues = try 53 - let merlin_start = Unix.gettimeofday () in 54 - let outline = Merlin.outline backend ~file:filename in 55 - let dump = Merlin.dump_ast backend ~file:filename in 56 - let merlin_duration = Unix.gettimeofday () -. merlin_start in 57 - (match profiling with Some prof -> Profiling.add_timing prof { operation = Profiling.Merlin filename; duration = merlin_duration } | None -> ()); 58 - let file_ctx = Context.file ~filename ~config ~project_root ~outline ~dump in 59 - let all_results = List.concat_map (run_file_rule ?profiling file_ctx) file_rules in 60 - List.filter (fun r -> 61 - let code = Rule.Run.code r in 62 - let skip = Rule_config.should_exclude config.exclusions ~rule:code ~file:filename in 63 - if skip then excluded_acc := { rule = code; file = filename } :: !excluded_acc; 64 - not skip) all_results 65 - with exn -> Log.err (fun m -> m "Failed to analyze %s: %s" filename (Printexc.to_string exn)); [] in 98 + let issues = 99 + try 100 + let merlin_start = Unix.gettimeofday () in 101 + let outline = Merlin.outline backend ~file:filename in 102 + let dump = Merlin.dump_ast backend ~file:filename in 103 + let merlin_duration = Unix.gettimeofday () -. merlin_start in 104 + (match profiling with 105 + | Some prof -> 106 + Profiling.add_timing prof 107 + { 108 + operation = Profiling.Merlin filename; 109 + duration = merlin_duration; 110 + } 111 + | None -> ()); 112 + let file_ctx = 113 + Context.file ~filename ~config ~project_root ~outline ~dump 114 + in 115 + let all_results = 116 + List.concat_map (run_file_rule ?profiling file_ctx) file_rules 117 + in 118 + List.filter 119 + (fun r -> 120 + let code = Rule.Run.code r in 121 + let skip = 122 + Rule_config.should_exclude config.exclusions ~rule:code 123 + ~file:filename 124 + in 125 + if skip then 126 + excluded_acc := { rule = code; file = filename } :: !excluded_acc; 127 + not skip) 128 + all_results 129 + with exn -> 130 + Log.err (fun m -> 131 + m "Failed to analyze %s: %s" filename (Printexc.to_string exn)); 132 + [] 133 + in 66 134 (issues, List.rev !excluded_acc) 67 135 68 136 let run ~filter ~dune_describe ?profiling project_root = 69 137 Log.info (fun m -> m "Starting analysis of %s" project_root); 70 - let config, files_to_analyze, project_ctx, enabled_rules = setup_analysis ~filter ~dune_describe project_root in 71 - let project_issues, project_excluded = run_project_rules ?profiling enabled_rules project_ctx in 138 + let config, files_to_analyze, project_ctx, enabled_rules = 139 + setup_analysis ~filter ~dune_describe project_root 140 + in 141 + let project_issues, project_excluded = 142 + run_project_rules ?profiling enabled_rules project_ctx 143 + in 72 144 let file_rules = List.filter Rule.is_file_scoped enabled_rules in 73 145 let backend = Merlin.v () in 74 - let analyze_file = analyze_single_file ?profiling ~backend ~config ~project_root ~file_rules in 146 + let analyze_file = 147 + analyze_single_file ?profiling ~backend ~config ~project_root ~file_rules 148 + in 75 149 let file_results = List.map analyze_file files_to_analyze in 76 150 Merlin.close backend; 77 151 let file_issues = List.concat_map fst file_results in 78 152 let file_excluded = List.concat_map snd file_results in 79 - { issues = List.sort Rule.Run.compare (project_issues @ file_issues); excluded = project_excluded @ file_excluded } 153 + { 154 + issues = List.sort Rule.Run.compare (project_issues @ file_issues); 155 + excluded = project_excluded @ file_excluded; 156 + }
+2 -8
lib/engine.mli
··· 1 1 (** Linting engine. *) 2 2 3 - type exclusion_stats = { 4 - rule : string; 5 - file : string; 6 - } 3 + type exclusion_stats = { rule : string; file : string } 7 4 (** A single suppressed issue. *) 8 5 9 - type result = { 10 - issues : Rule.Run.result list; 11 - excluded : exclusion_stats list; 12 - } 6 + type result = { issues : Rule.Run.result list; excluded : exclusion_stats list } 13 7 (** Analysis result. *) 14 8 15 9 val run :