Skip to content

Commit 1c3a819

Browse files
hlshipeca-agent
andcommitted
feat: detect circular interface implements chains
- interface implementing itself is caught in compile-type :interface - indirect cycles (A->B->C->A) caught in prepare-and-validate-interfaces by checking if an interface appears in its own transitive implements set 🤖 Generated with [eca](https://eca.dev) Co-Authored-By: eca-agent <git@eca.dev>
1 parent 2fe36e3 commit 1c3a819

2 files changed

Lines changed: 45 additions & 5 deletions

File tree

src/com/walmartlabs/lacinia/schema.clj

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,20 +1584,25 @@
15841584

15851585
(defmethod compile-type :interface
15861586
[interface schema]
1587-
(let [implements (->> interface :implements (map as-keyword) set)]
1587+
(let [interface-name (:type-name interface)
1588+
implements (->> interface :implements (map as-keyword) set)]
15881589
(doseq [iface-name implements
15891590
:let [type (get schema iface-name)]]
1591+
(when (= iface-name interface-name)
1592+
(throw (ex-info (format "Interface %s cannot implement itself."
1593+
(q iface-name))
1594+
{:interface interface-name})))
15901595
(when-not type
15911596
(throw (ex-info (format "Interface %s implements interface %s, which does not exist."
1592-
(-> interface :type-name q)
1597+
(q interface-name)
15931598
(q iface-name))
1594-
{:interface (:type-name interface)
1599+
{:interface interface-name
15951600
:schema-types (type-map schema)})))
15961601
(when-not (= :interface (:category type))
15971602
(throw (ex-info (format "Interface %s implements type %s, which is not an interface."
1598-
(-> interface :type-name q)
1603+
(q interface-name)
15991604
(q iface-name))
1600-
{:interface (:type-name interface)
1605+
{:interface interface-name
16011606
:schema-types (type-map schema)}))))
16021607
(->> interface
16031608
map->Interface
@@ -1787,6 +1792,14 @@
17871792
[schema]
17881793
(let [objects (types-with-category schema :object)
17891794
interfaces (types-with-category schema :interface)
1795+
;; Detect cycles in the interface implements graph before doing anything else.
1796+
_ (doseq [interface interfaces
1797+
:let [interface-name (:type-name interface)
1798+
transitive (all-implemented-interfaces schema interface-name)]]
1799+
(when (transitive interface-name)
1800+
(throw (ex-info (format "Interface %s is part of a circular implements chain."
1801+
(q interface-name))
1802+
{:interface interface-name}))))
17901803
;; Expand each object's :implements set to include transitively-inherited interfaces.
17911804
;; This is needed so check-compatible [:interface :object] works when an object only
17921805
;; directly lists a sub-interface but not its parent interfaces.

test/com/walmartlabs/interface_test.clj

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,33 @@
134134
:parent-interface-name :node}
135135
(compile invalid-schema))))
136136

137+
(deftest interface-circular-implements-fails
138+
(testing "direct cycle (A implements B, B implements A)"
139+
(let [invalid-schema '{:interfaces {:A {:implements [:B]
140+
:fields {:id {:type String}}}
141+
:B {:implements [:A]
142+
:fields {:id {:type String}}}}}]
143+
(is (thrown-with-msg? Throwable #"circular implements chain"
144+
(compile invalid-schema)))))
145+
146+
(testing "indirect cycle (A implements B, B implements C, C implements A)"
147+
(let [invalid-schema '{:interfaces {:A {:implements [:B]
148+
:fields {:id {:type String}}}
149+
:B {:implements [:C]
150+
:fields {:id {:type String}}}
151+
:C {:implements [:A]
152+
:fields {:id {:type String}}}}}]
153+
(is (thrown-with-msg? Throwable #"circular implements chain"
154+
(compile invalid-schema))))))
155+
156+
(deftest interface-cannot-implement-itself
157+
(let [invalid-schema '{:interfaces {:node {:implements [:node]
158+
:fields {:id {:type String}}}}}]
159+
(expect-exception
160+
"Interface `node' cannot implement itself."
161+
{:interface :node}
162+
(compile invalid-schema))))
163+
137164
(deftest interface-implements-non-interface-fails
138165
;; :resource tries to implement :article which is an object, not an interface
139166
(let [invalid-schema '{:interfaces {:node {:fields {:id {:type String}}}

0 commit comments

Comments
 (0)