package crs

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Source file config.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
(********************************************************************************)
(*  crs - A tool for managing code review comments embedded in source code      *)
(*  Copyright (C) 2024-2025 Mathieu Barbin <mathieu.barbin@gmail.com>           *)
(*                                                                              *)
(*  This file is part of crs.                                                   *)
(*                                                                              *)
(*  crs is free software; you can redistribute it and/or modify it under the    *)
(*  terms of the GNU Lesser General Public License as published by the Free     *)
(*  Software Foundation either version 3 of the License, or any later version,  *)
(*  with the LGPL-3.0 Linking Exception.                                        *)
(*                                                                              *)
(*  crs is distributed in the hope that it will be useful, but WITHOUT ANY      *)
(*  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS   *)
(*  FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License and     *)
(*  the file `NOTICE.md` at the root of this repository for more details.       *)
(*                                                                              *)
(*  You should have received a copy of the GNU Lesser General Public License    *)
(*  and the LGPL-3.0 Linking Exception along with this library. If not, see     *)
(*  <http://www.gnu.org/licenses/> and <https://spdx.org>, respectively.        *)
(********************************************************************************)

open! Import

module Annotation_severity = struct
  type t =
    | Error
    | Warning
    | Info
  [@@deriving sexp_of]

  let of_string = function
    | "Error" -> Some Error
    | "Warning" -> Some Warning
    | "Info" -> Some Info
    | _ -> None
  ;;
end

module User_handle = struct
  type t = Vcs.User_handle.t [@@deriving sexp_of]

  let of_yojson json =
    match (json : Yojson.Safe.t) with
    | `String str ->
      (match Vcs.User_handle.of_string str with
       | Ok _ as ok -> ok
       | Error (`Msg msg) -> Error msg)
    | _ -> Error "User handle expected to be a json string."
  ;;
end

module User_list = struct
  type t = User_handle.t list

  let of_yojson json : (t, string) Result.t =
    match (json : Yojson.Safe.t) with
    | `List xs -> Ppx_deriving_yojson_runtime.map_bind User_handle.of_yojson [] xs
    | _ -> Error "User handle list expected to be a list of json strings."
  ;;
end

type t =
  { default_repo_owner : User_handle.t option [@sexp.option]
  ; user_mentions_allowlist : User_handle.t list option [@sexp.option]
  ; invalid_crs_annotation_severity : Annotation_severity.t option [@sexp.option]
  ; crs_due_now_annotation_severity : Annotation_severity.t option [@sexp.option]
  }
[@@deriving sexp_of]

let get_json_field ~loc ~fields ~field_name =
  match
    List.filter_map fields ~f:(fun (name, json) ->
      Option.some_if (String.equal name field_name) json)
  with
  | [] -> None
  | [ f ] -> Some f
  | _ :: _ :: _ ->
    Err.raise
      ~loc
      Pp.O.
        [ Pp.text "Json object field "
          ++ Pp_tty.kwd (module String) field_name
          ++ Pp.text " is duplicated in this config."
        ]
;;

let get_json_enum_constructor json ~loc ~field_name =
  match json with
  | `String str -> `Unwrapped str
  | `List [ `String str ] -> `Wrapped str
  | _ ->
    Err.raise
      ~loc
      Pp.O.
        [ Pp.text "In: " ++ Pp.text (Yojson.Safe.to_string json)
        ; Pp.text "Field "
          ++ Pp_tty.kwd (module String) field_name
          ++ Pp.text " expected to be a json string."
        ]
;;

let parse_json json ~loc ~emit_github_annotations =
  let of_yojson_exn f json =
    match f json with
    | Ok x -> x
    | Error msg ->
      Err.raise
        ~loc
        Pp.O.
          [ Pp.text "Invalid config."
          ; Pp.text "In: " ++ Pp.text (Yojson.Safe.to_string json)
          ; Pp.text msg
          ]
  in
  match json with
  | `Assoc fields ->
    (* Track which fields have been accessed *)
    let used_fields = Hash_set.create (module String) in
    let field field_name =
      Hash_set.add used_fields field_name;
      get_json_field ~loc ~fields ~field_name
    in
    let default_repo_owner =
      match field "default_repo_owner" with
      | Some json -> Some (of_yojson_exn User_handle.of_yojson json)
      | None -> None
    in
    let user_mentions_allowlist =
      let field_name = "user_mentions_allowlist" in
      match field field_name with
      | Some json -> Some (of_yojson_exn User_list.of_yojson json)
      | None ->
        (* See [upgrading-crs] guide in the documentation for more details about
           deprecated fields and compatibility transitions in the configs. *)
        let deprecated_field_name = "user_mentions_whitelist" in
        (match field deprecated_field_name with
         | None -> None
         | Some json ->
           User_message.warning
             ~loc
             ~emit_github_annotations
             Pp.O.
               [ Pp.text "The config field name "
                 ++ Pp_tty.kwd (module String) deprecated_field_name
                 ++ Pp.text " is deprecated and was renamed "
                 ++ Pp_tty.kwd (module String) field_name
                 ++ Pp.text "."
               ]
             ~hints:[ Pp.text "Upgrade the config to use the new name." ];
           Some (of_yojson_exn User_list.of_yojson json))
    in
    let severity_field ~field_name =
      match field field_name with
      | None -> None
      | Some json ->
        let parse_string str =
          match Annotation_severity.of_string str with
          | Some t -> t
          | None ->
            Err.raise
              ~loc
              Pp.O.
                [ Pp.text "Field " ++ Pp_tty.kwd (module String) field_name ++ Pp.text ":"
                ; Pp.textf "Unsupported annotation severity %S." str
                ]
        in
        (match get_json_enum_constructor json ~loc ~field_name with
         | `Unwrapped str -> Some (parse_string str)
         | `Wrapped str ->
           let severity = parse_string str in
           User_message.warning
             ~loc
             ~emit_github_annotations
             Pp.O.
               [ Pp.text "The config field name "
                 ++ Pp_tty.kwd (module String) field_name
                 ++ Pp.text " is now expected to be a json string rather than a list."
               ]
             ~hints:[ Pp.textf "Change it to simply: %S" str ];
           Some severity)
    in
    let invalid_crs_annotation_severity =
      severity_field ~field_name:"invalid_crs_annotation_severity"
    in
    let crs_due_now_annotation_severity =
      severity_field ~field_name:"crs_due_now_annotation_severity"
    in
    (* Emit warnings for any unknown fields *)
    List.iter fields ~f:(fun (name, _) ->
      if not (Hash_set.mem used_fields name)
      then
        User_message.warning
          ~loc
          ~emit_github_annotations
          Pp.O.[ Pp.text "Unknown config field: " ++ Pp_tty.kwd (module String) name ]
          ~hints:[ Pp.text "Check the documentation for valid field names." ]);
    { default_repo_owner
    ; user_mentions_allowlist
    ; invalid_crs_annotation_severity
    ; crs_due_now_annotation_severity
    }
  | _ -> Err.raise ~loc [ Pp.text "Config expected to be a json object." ]
;;

let default_repo_owner t = t.default_repo_owner
let user_mentions_allowlist t = t.user_mentions_allowlist
let invalid_crs_annotation_severity t = t.invalid_crs_annotation_severity
let crs_due_now_annotation_severity t = t.crs_due_now_annotation_severity

let create
      ?default_repo_owner
      ?user_mentions_allowlist
      ?invalid_crs_annotation_severity
      ?crs_due_now_annotation_severity
      ()
  =
  { default_repo_owner
  ; user_mentions_allowlist
  ; invalid_crs_annotation_severity
  ; crs_due_now_annotation_severity
  }
;;

let empty =
  { default_repo_owner = None
  ; user_mentions_allowlist = None
  ; invalid_crs_annotation_severity = None
  ; crs_due_now_annotation_severity = None
  }
;;

let load_exn ~path ~emit_github_annotations =
  match Yojson_five.Safe.from_file (Fpath.to_string path) with
  | Ok json -> parse_json json ~loc:(Loc.of_file ~path) ~emit_github_annotations
  | Error msg ->
    Err.raise ~loc:(Loc.of_file ~path) [ Pp.text "Not a valid json file."; Pp.text msg ]
;;