Skip to content

Commit 5a73864

Browse files
Bruce Haumanclaude
andcommitted
Replace clj-kondo with edamame for delimiter checking
Simplifies delimiter error detection by replacing comprehensive clj-kondo linting with edamame's focused delimiter checking. This reduces dependencies and overhead while maintaining reliable detection of unbalanced parentheses, brackets, braces, and quotes. Semantic errors are now caught at evaluation time rather than pre-evaluation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e3d4cae commit 5a73864

File tree

14 files changed

+139
-239
lines changed

14 files changed

+139
-239
lines changed

deps.edn

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
;; Clojure source manipulation
1616
rewrite-clj/rewrite-clj {:mvn/version "1.1.47"}
1717
dev.weavejester/cljfmt {:mvn/version "0.13.1"}
18-
clj-kondo/clj-kondo {:mvn/version "2024.03.13"}
18+
borkdude/edamame {:mvn/version "1.5.35"}
1919
org.clojars.oakes/parinfer {:mvn/version "0.4.0"}
2020

2121
;; llm ecosystem java libs
@@ -146,8 +146,6 @@
146146
{:git/tag "v0.9.2" :git/sha "fe6b140"}
147147
slipset/deps-deploy {:mvn/version "0.2.0"}}
148148
:ns-default build}
149-
:neil {:project {:name clojure-mcp/clojure-mcp}}
150-
:lint {:deps {clj-kondo/clj-kondo {:mvn/version "2025.02.20"}}
151-
:main-opts ["-m" "clj-kondo.main" "--lint" "src"]}}}
149+
:neil {:project {:name clojure-mcp/clojure-mcp}}}}
152150

153151

src/clojure_mcp/delimiter.clj

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
(ns clojure-mcp.delimiter
2+
"Delimiter error checking using edamame parser."
3+
(:require [edamame.core :as e]))
4+
5+
(defn delimiter-error?
6+
"Returns true if the string has a delimiter error specifically.
7+
Checks both that it's an :edamame/error and has delimiter info.
8+
Uses :all true to enable all standard Clojure reader features:
9+
function literals, regex, quotes, syntax-quote, deref, var, etc.
10+
Also enables :read-cond :allow to support reader conditionals.
11+
Handles unknown data readers gracefully with a default reader fn."
12+
[s]
13+
(try
14+
(e/parse-string-all s {:all true
15+
:features #{:bb :clj :cljs :cljr :default}
16+
:read-cond :allow
17+
:readers (fn [_tag] (fn [data] data))
18+
:auto-resolve name})
19+
false ; No error = no delimiter error
20+
(catch clojure.lang.ExceptionInfo ex
21+
(let [data (ex-data ex)]
22+
(and (= :edamame/error (:type data))
23+
(contains? data :edamame/opened-delimiter))))
24+
(catch Exception _e
25+
;; For other exceptions, conservatively return true
26+
;; to allow potential delimiter repair attempts
27+
true)))
28+
29+
(defn count-forms
30+
"Counts the number of top-level forms in a string using edamame.
31+
Returns the count of forms, or throws an exception if parsing fails."
32+
[s]
33+
(try
34+
(count (e/parse-string-all s {:all true
35+
:features #{:bb :clj :cljs :cljr :default}
36+
:read-cond :allow
37+
:readers (fn [_tag] (fn [data] data))
38+
:auto-resolve name}))
39+
(catch Exception e
40+
(throw (ex-info "Failed to parse forms"
41+
{:error (ex-message e)}
42+
e)))))

src/clojure_mcp/linting.clj

Lines changed: 0 additions & 87 deletions
This file was deleted.

src/clojure_mcp/sexp/paren_utils.clj

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(ns clojure-mcp.sexp.paren-utils
22
(:require
3-
[clojure-mcp.linting :as linting]
3+
[clojure-mcp.delimiter :as delimiter]
44
[rewrite-clj.parser :as parser]
55
[rewrite-clj.node :as node]
66
[clojure.string :as string])
@@ -92,44 +92,15 @@
9292
(defn parinfer-repair [code-str]
9393
(let [res (Parinfer/indentMode code-str nil nil nil false)]
9494
(when (and (.success res)
95-
(not (:error? (linting/lint (.text res)))))
95+
(not (delimiter/delimiter-error? (.text res))))
9696
(.text res))))
9797

98-
(def delimiter-error-patterns [#"Unmatched bracket"
99-
#"Found an opening .* with no matching"
100-
#"Expected a .* to match"
101-
#"Mismatched bracket"
102-
#"Unexpected EOF while reading"
103-
#"Unmatched opening"
104-
#"Unmatched closing"])
105-
106-
(defn has-delimiter-errors? [{:keys [report error?] :as lint-result}]
107-
(if (and lint-result error?)
108-
(boolean (some #(re-find % report) delimiter-error-patterns))
109-
false))
110-
11198
(defn code-has-delimiter-errors?
11299
"Returns true if the given Clojure code string has delimiter errors
113100
(unbalanced parentheses, brackets, braces, or string quotes).
114101
Returns false otherwise."
115102
[code-str]
116-
(let [lint-result (linting/lint-delims code-str)]
117-
(has-delimiter-errors? lint-result)))
118-
119-
#_(defn smart-repair [code-str]
120-
(if-let [parinfer-result (parinfer-repair code-str)]
121-
(if-let [lint-result (linting/lint parinfer-result)]
122-
{:repaired? true
123-
:form parinfer-result
124-
:message "Parenthesis repaired using parinfer"}
125-
;; Parinfer produced code with semantic issues, try homegrown approach
126-
(or (repair-parens code-str)
127-
;; If homegrown fails too, return parinfer result with a warning
128-
{:repaired? true
129-
:form parinfer-result
130-
:message "Warning: Fixed syntax but may have changed semantics"}))
131-
;; Parinfer failed (unlikely), fall back to homegrown
132-
(repair-parens code-str)))
103+
(delimiter/delimiter-error? code-str))
133104

134105
(comment
135106
(def code1 "(defn hello [name] (str \"Hello\" name)))")

src/clojure_mcp/tools/eval/core.clj

Lines changed: 44 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This namespace contains the pure functionality without any MCP-specific code."
44
(:require
55
[clojure-mcp.nrepl :as nrepl]
6-
[clojure-mcp.linting :as linting]
6+
[clojure-mcp.delimiter :as delimiter]
77
[clojure-mcp.sexp.paren-utils :as paren-utils]
88
[clojure.string :as string]
99
[clojure.tools.logging :as log]))
@@ -36,10 +36,9 @@
3636

3737
(defn repair-code
3838
[code]
39-
(let [linted (linting/lint code)]
40-
(if (and linted (:error? linted) (paren-utils/has-delimiter-errors? linted))
41-
(or (paren-utils/parinfer-repair code) code)
42-
code)))
39+
(if (delimiter/delimiter-error? code)
40+
(or (paren-utils/parinfer-repair code) code)
41+
code))
4342

4443
(defn evaluate-code
4544
"Evaluates Clojure code using the nREPL client.
@@ -58,67 +57,55 @@
5857
outputs (atom [])
5958
error-occurred (atom false)
6059
form-str code
61-
linted (linting/lint form-str)
6260
add-output! (fn [prefix value] (swap! outputs conj [prefix value]))
6361
result-promise (promise)]
6462

65-
;; Add linter output if present
66-
(when linted
67-
(add-output! :lint (:report linted))
68-
(when (:error? linted)
69-
(reset! error-occurred true)))
70-
71-
;; If linter found critical errors, return early
72-
(if @error-occurred
73-
{:outputs @outputs
74-
:error true}
75-
76-
;; Otherwise, evaluate the code
77-
(do
63+
;; Evaluate the code
64+
(do
7865
;; Push to eval history if available
79-
(when-let [state (::nrepl/state nrepl-client)]
80-
(swap! state update :clojure-mcp.repl-tools/eval-history conj form-str))
66+
(when-let [state (::nrepl/state nrepl-client)]
67+
(swap! state update :clojure-mcp.repl-tools/eval-history conj form-str))
8168

8269
;; Evaluate the code, using the namespace parameter if provided
83-
(try
84-
(nrepl/eval-code-msg
85-
nrepl-client form-str
86-
(if session {:session session} {})
87-
(->> identity
88-
(nrepl/out-err
89-
#(add-output! :out %)
90-
#(add-output! :err %))
91-
(nrepl/value #(add-output! :value %))
92-
(nrepl/done (fn [_]
93-
(deliver result-promise
94-
{:outputs @outputs
95-
:error @error-occurred})))
96-
(nrepl/error (fn [{:keys [exception]}]
97-
(reset! error-occurred true)
98-
(add-output! :err exception)
99-
(deliver result-promise
100-
{:outputs @outputs
101-
:error true})))))
102-
(catch Exception e
70+
(try
71+
(nrepl/eval-code-msg
72+
nrepl-client form-str
73+
(if session {:session session} {})
74+
(->> identity
75+
(nrepl/out-err
76+
#(add-output! :out %)
77+
#(add-output! :err %))
78+
(nrepl/value #(add-output! :value %))
79+
(nrepl/done (fn [_]
80+
(deliver result-promise
81+
{:outputs @outputs
82+
:error @error-occurred})))
83+
(nrepl/error (fn [{:keys [exception]}]
84+
(reset! error-occurred true)
85+
(add-output! :err exception)
86+
(deliver result-promise
87+
{:outputs @outputs
88+
:error true})))))
89+
(catch Exception e
10390
;; prevent connection errors from confusing the LLM
104-
(log/error e "Error when trying to eval on the nrepl connection")
105-
(throw
106-
(ex-info
107-
(str "Internal Error: Unable to reach the nREPL "
108-
"thus we are unable to execute the bash command.")
109-
{:error-type :connection-error}
110-
e))))
91+
(log/error e "Error when trying to eval on the nrepl connection")
92+
(throw
93+
(ex-info
94+
(str "Internal Error: Unable to reach the nREPL "
95+
"thus we are unable to execute the bash command.")
96+
{:error-type :connection-error}
97+
e))))
11198

11299
;; Wait for the result and return it
113-
(let [tmb (Object.)
114-
res (deref result-promise timeout-ms tmb)]
115-
(if-not (= tmb res)
116-
res
117-
(do
118-
(nrepl/interrupt nrepl-client)
119-
{:outputs [[:err (str "Eval timed out after " timeout-ms "ms.")]
120-
[:err "Perhaps, you had an infinite loop or an eval that ran too long."]]
121-
:error true})))))))
100+
(let [tmb (Object.)
101+
res (deref result-promise timeout-ms tmb)]
102+
(if-not (= tmb res)
103+
res
104+
(do
105+
(nrepl/interrupt nrepl-client)
106+
{:outputs [[:err (str "Eval timed out after " timeout-ms "ms.")]
107+
[:err "Perhaps, you had an infinite loop or an eval that ran too long."]]
108+
:error true}))))))
122109

123110
(defn evaluate-with-repair
124111
"Evaluates Clojure code with automatic repair of delimiter errors.

src/clojure_mcp/tools/file_edit/pipeline.clj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
[clojure-mcp.tools.agent-tool-builder.file-changes :as file-changes]
1111
[clojure-mcp.tools.unified-read-file.file-timestamps :as file-timestamps]
1212
[clojure-mcp.config :as config]
13-
[clojure-mcp.linting :as linting]
1413
[clojure.spec.alpha :as s]
1514
[clojure.string :as str]
1615
[clojure.java.io :as io]

src/clojure_mcp/tools/file_write/core.clj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
[clojure.java.io :as io]
66
[clojure-mcp.tools.form-edit.pipeline :as pipeline]
77
[clojure-mcp.utils.diff :as diff-utils]
8-
[clojure-mcp.linting :as linting]
98
[clojure-mcp.utils.valid-paths :as valid-paths]
109
[rewrite-clj.zip :as z]
1110
[clojure-mcp.utils.file :as file-utils]))

0 commit comments

Comments
 (0)