package dunolint

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

Source file sexp_handler.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
(*********************************************************************************)
(*  Dunolint - A tool to lint and help manage files in dune projects             *)
(*  Copyright (C) 2024-2025 Mathieu Barbin <mathieu.barbin@gmail.com>            *)
(*                                                                               *)
(*  This file is part of Dunolint.                                               *)
(*                                                                               *)
(*  Dunolint 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.                                *)
(*                                                                               *)
(*  Dunolint 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.         *)
(*********************************************************************************)

module type S = Sexp_handler_intf.S

let replace_field ~sexps_rewriter ~field ~new_field =
  let file_rewriter = Sexps_rewriter.file_rewriter sexps_rewriter in
  if not ([%equal: Sexp.t] field new_field)
  then
    File_rewriter.replace
      file_rewriter
      ~range:(Sexps_rewriter.range sexps_rewriter field)
      ~text:(Sexp.to_string_hum new_field)
;;

let find (type a) (module M : S with type t = a) ~sexps_rewriter ~fields =
  List.find_map fields ~f:(fun field ->
    match (field : Sexp.t) with
    | List (Atom name :: _) when String.equal name M.field_name ->
      Some (M.read ~sexps_rewriter ~field)
    | _ -> None)
;;

let get_args ~field_name ~sexps_rewriter ~field =
  match field with
  | Sexp.List (Atom field_name' :: args) when String.equal field_name field_name' -> args
  | _ ->
    Err.raise
      ~loc:(Sexps_rewriter.loc sexps_rewriter field)
      Pp.O.
        [ Pp.text "Unexpected "
          ++ Pp_tty.kwd (module String) field_name
          ++ Pp.text " field."
        ]
;;

module Make_sexpable
    (M : sig
       val field_name : string
     end)
    (S : Sexpable.S) =
struct
  type t = S.t [@@deriving sexp_of]

  let field_name = M.field_name

  let read ~sexps_rewriter ~field =
    match field with
    | Sexp.List [ Atom field_name; value ] when String.equal field_name M.field_name ->
      S.t_of_sexp value
    | _ ->
      Err.raise
        ~loc:(Sexps_rewriter.loc sexps_rewriter field)
        Pp.O.
          [ Pp.textf "Unexpected [Sexp] for field "
            ++ Pp_tty.kwd (module String) M.field_name
            ++ Pp.text "."
          ]
  ;;

  let write (t : t) = Sexp.List [ Atom M.field_name; S.sexp_of_t t ]

  let rewrite (t : t) ~sexps_rewriter ~field =
    replace_field ~sexps_rewriter ~field ~new_field:(write t)
  ;;
end

module Make_atom (M : sig
    val field_name : string
  end) =
  Make_sexpable (M) (String)

module Make_sexps (M : sig
    val field_name : string
  end) =
struct
  type t = Sexp.t list [@@deriving sexp_of]

  let field_name = M.field_name

  let read ~sexps_rewriter ~field =
    get_args ~field_name:M.field_name ~sexps_rewriter ~field
  ;;

  let write (t : t) = Sexp.List (Atom M.field_name :: t)

  let rewrite (t : t) ~sexps_rewriter ~field =
    replace_field ~sexps_rewriter ~field ~new_field:(write t)
  ;;
end

let insert_new_fields ~sexps_rewriter ~indicative_field_ordering ~fields ~new_fields =
  let new_fields =
    List.map new_fields ~f:(fun (field : Sexp.t) ->
      let name =
        match field with
        | List (Atom name :: _) -> name
        | _ ->
          Err.raise
            [ Pp.text "Unexpected field shape"; Pp.text (Sexp.to_string_hum field) ]
      in
      ref false, name, field)
  in
  let file_rewriter = Sexps_rewriter.file_rewriter sexps_rewriter in
  (* We insert all missing fields. *)
  List.iter fields ~f:(fun field ->
    match (field : Sexp.t) with
    | List (Atom field :: _) ->
      List.iter new_fields ~f:(fun (visited, field_name, _) ->
        if String.equal field field_name then visited := true)
    | _ -> ());
  List.iter new_fields ~f:(fun (visited, field_name, new_field) ->
    if not !visited
    then (
      (* To compute the place of insertion we skip input fields as long as they
         appear prior to this field. When we can no longer, we insert after the
         last one. *)
      let field_names_located_before =
        let rec aux acc = function
          | [] -> acc
          | hd :: tl ->
            if String.equal hd field_name then acc else aux (Set.add acc hd) tl
        in
        aux (Set.empty (module String)) indicative_field_ordering
      in
      let pred_field =
        let rec aux last = function
          | [] -> last
          | hd :: tl ->
            (match (hd : Sexp.t) with
             | List (Atom name :: _) ->
               if Set.mem field_names_located_before name then aux hd tl else last
             | _ -> last)
        in
        match fields with
        | hd :: tl -> aux hd tl
        | [] ->
          Err.raise
            ~loc:(Loc.of_file ~path:(Sexps_rewriter.path sexps_rewriter))
            [ Pp.textf "Existing stanza in dune file expected to have at least one field."
            ]
      in
      let pred_loc = Sexps_rewriter.loc sexps_rewriter pred_field in
      let indentation =
        (* The intention here is to help when auto fmt is not
           available for that dune file. *)
        let pred_pos = Loc.start pred_loc in
        let pred_indent = pred_pos.pos_cnum - pred_pos.pos_bol in
        String.make pred_indent ' '
      in
      File_rewriter.insert
        file_rewriter
        ~offset:(Loc.stop_offset pred_loc)
        ~text:("\n" ^ indentation ^ Sexp.to_string_hum new_field)))
;;
OCaml

Innovation. Community. Security.