borkdude/edamame

Using a custom :map parsing function has no access to location data

NoahTheDuke opened this issue · 5 comments

This isn't quite a bug, but a quirk I noticed. If I use the :map function to make a custom map container (ordered map or whatever), I only get the elements, no loc information. This means that I can't throw any exceptions that include location information in the message/ex-data.

I don't know if there are any good solutions here, just wanted to get your thoughts. (There's ways around this, they're just super annoying lol.)

That is true of almost all of the other options as well, but the resulting structure will have the metadata attached, which is generally how it works:

 (meta (e/parse-string "{:a 1 :b 2}" {:map (fn [& args] args)}))
{:row 1, :col 1, :end-row 1, :end-col 12}

You can get at the location of the map (or rather, after the map) as follows:

user=> (e/parse-next rdr {:map (fn [& args] (prn (e/get-line-number rdr) (e/get-column-number rdr)) args)})
1 12
(:a 1 :b 2)

Another way is to use the :postprocess hook:

 (e/parse-string "{:a 1 :b 2}" {:postprocess (fn [x] (prn (:obj x) (:loc x)) (:obj x)) :map (fn [& args] args)})
:a {:row 1, :col 2, :end-row 1, :end-col 4}
1 {:row 1, :col 5, :end-row 1, :end-col 6}
:b {:row 1, :col 7, :end-row 1, :end-col 9}
2 {:row 1, :col 10, :end-row 1, :end-col 11}
(:a 1 :b 2) {:row 1, :col 1, :end-row 1, :end-col 12}
(:a 1 :b 2)

If you notice that a parsed map is invalid somehow, you can construct some artificial object (deftype or whatever), and then check in the postprocess hook if there is such an artificial object and throw with the specified location.

That's basically what I'm doing right now:

(def ^:private rdr (e/reader ""))
(deftype ParseMap [elements])
(deftype ParseSet [elements])

(defn parse-map
  [^ParseMap obj loc]
  (let [elements (.elements obj)
        c (count elements)]
    (when (pos? c)
      (when (odd? c)
        (e.i.p/throw-odd-map {:row-key :line
                              :col-key :column}
          rdr {:col (:column loc)
               :row (:line loc)}
          elements))
      (let [ks (take-nth 2 elements)]
        (when-not (apply distinct? ks)
          (e.i.p/throw-dup-keys {:row-key :line
                                 :col-key :column}
            rdr {:col (:column loc)
                 :row (:line loc)}
            :map ks))))
    (apply om/ordered-map elements)))

(defn parse-set
  [^ParseSet obj loc]
  (let [elements (.elements obj)
        the-set (apply os/ordered-set elements)]
    (when-not (= (count elements) (count the-set))
      (e.i.p/throw-dup-keys {:row-key :line
                             :col-key :column}
        rdr {:col (:column loc)
             :row (:line loc)}
        :set elements))
    the-set))

and then

   :postprocess (fn postprocess [{:keys [obj loc]}]
                  (cond-> obj
                    (instance? ParseMap obj) (parse-map loc)
                    (instance? ParseSet obj) (parse-set loc)
                    (instance? clojure.lang.IObj obj)
                    (-> (vary-meta merge loc)
                      (attach-import-meta ns-state))))
   :map (fn [& elements] (ParseMap. elements))
   :set (fn [& elements] (ParseSet. elements))

Seems like a good workaround. I'll close this issue then.