Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ The configurations are merged in the following order, where a later config overr

You can find all settings and its default values [here](../src/clj_depend/config.clj) and below the docs for each one:

### :level

Severity level of violations, if any.

Default: `:error`.

Config example:
```clojure
{,,,
:level :error
,,,}
```

### :source-paths

Directories within the project to look for clj files. Files outside the source-paths will be ignored.
Expand Down Expand Up @@ -61,6 +74,7 @@ Defining the rules for namespaces.
Default: `[]`.

A vector of rules whose each rule is a map composed of the following fields:

- `:defined-by` optional, a regular expression (regex) that serves as a predicate to identify whether the rule should be evaluated.
- `:namespaces` optional, a set of namespaces that serves as a predicate to identify whether the rule should be evaluated.
- `:should-not-depend-on` required, a set of namespaces or regular expressions (regex).
Expand Down
10 changes: 1 addition & 9 deletions src/clj_depend/analyzers/layer.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
(ns clj-depend.analyzers.layer
(:require [clojure.set :as set]))
(ns clj-depend.analyzers.layer)

(set! *warn-on-reflection* true)

Expand Down Expand Up @@ -53,13 +52,6 @@
:dependency-namespace dependency-namespace
:dependency-layer (layer-by-namespace config dependency-namespace dependencies-by-namespace)}))

(defn ^:private namespace-dependencies
[{:keys [only-ns-in-source-paths]} namespace dependencies-by-namespace]
(let [namespace-dependencies (get dependencies-by-namespace namespace)]
(if only-ns-in-source-paths
(set/intersection (set namespace-dependencies) (set (keys dependencies-by-namespace)))
namespace-dependencies)))

(defn analyze
[config namespace dependencies-by-namespace]
(->> (get dependencies-by-namespace namespace)
Expand Down
1 change: 1 addition & 0 deletions src/clj_depend/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

(def ^:private default-config
{:source-paths #{"src"}
:level :error
:layers {}
:rules []})

Expand Down
32 changes: 17 additions & 15 deletions src/clj_depend/internal_api.clj
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
(ns clj-depend.internal-api
(:require [clj-depend.analyzer :as analyzer]
[clj-depend.config :as config]
[clj-depend.snapshot :as snapshot]
[clojure.java.io :as io]
[clojure.string :as string]
[clojure.tools.namespace.file :as file]
[clojure.tools.namespace.find :as namespace.find]
[clojure.tools.namespace.parse :as namespace.parse]))
(:require
[clj-depend.analyzer :as analyzer]
[clj-depend.config :as config]
[clj-depend.snapshot :as snapshot]
[clojure.java.io :as io]
[clojure.string :as string]
[clojure.tools.namespace.file :as file]
[clojure.tools.namespace.find :as namespace.find]
[clojure.tools.namespace.parse :as namespace.parse]))

(set! *warn-on-reflection* true)

Expand Down Expand Up @@ -90,13 +91,14 @@
(let [context (build-context options)
violations (analyzer/analyze context)
_ (snapshot/dump-when-enabled! violations options)
violations (snapshot/without-violations-present-in-snapshot-file! violations options)]
(if (seq violations)
{:result-code 1
:message (string/join "\n" (map :message violations))
:violations violations}
{:result-code 0
:message "No violations found!"}))
violations (->> (snapshot/without-violations-present-in-snapshot-file! violations options)
(map #(assoc % :level (-> context :config :level))))
errors? (some (comp #(= :error %) :level) violations)]
(cond-> {:result-code (if errors? 1 0)
:message (if (empty? violations)
"No violations found!"
(string/join "\n" (map :message violations)))}
(not-empty violations) (assoc :violations violations)))
(catch Exception e
{:result-code 2
:message (ex-message e)})))
84 changes: 72 additions & 12 deletions test/clj_depend/api_integration_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@
:message "Circular dependency between \"sample.controller.foo\" and \"sample.logic.foo\"\nCircular dependency between \"sample.logic.foo\" and \"sample.controller.foo\"\n\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:violations [{:namespace 'sample.controller.foo
:dependency-namespace 'sample.logic.foo
:message "Circular dependency between \"sample.controller.foo\" and \"sample.logic.foo\""}
:message "Circular dependency between \"sample.controller.foo\" and \"sample.logic.foo\""

Copilot AI Jun 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Duplicate violation message strings appear across tests; consider extracting them into a shared constant to ease maintenance in case the message needs updating.

Suggested change
:message "Circular dependency between \"sample.controller.foo\" and \"sample.logic.foo\""
:message circular-dependency-message

Copilot uses AI. Check for mistakes.
:level :error}
{:namespace 'sample.logic.foo
:dependency-namespace 'sample.controller.foo
:message "Circular dependency between \"sample.logic.foo\" and \"sample.controller.foo\""}
:message "Circular dependency between \"sample.logic.foo\" and \"sample.controller.foo\""
:level :error}
{:namespace 'sample.logic.foo
:dependency-namespace 'sample.controller.foo
:layer :logic
:dependency-layer :controller
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"}]}
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:level :error}]}
(api/analyze {:project-root (io/file (io/resource "with-cyclic-dependency"))})))))

(deftest analyze-project-without-violations
Expand All @@ -45,9 +48,58 @@
:dependency-namespace 'sample.controller.foo
:layer :logic
:dependency-layer :controller
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"}]}
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:level :error}]}
(api/analyze {:project-root (io/file (io/resource "with-violations"))})))))

(deftest analyze-project-with-violation-with-different-levels
(testing "should return result code 1 when there is at least one violation with level equal to :error and there is no level in the config"
(is (= {:result-code 1
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:violations [{:namespace 'sample.logic.foo
:dependency-namespace 'sample.controller.foo
:layer :logic
:dependency-layer :controller
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:level :error}]}
(api/analyze {:project-root (io/file (io/resource "with-violations"))}))))

(testing "should return result code 1 when there is at least one violation with level equal to :error and there the level in the config is :error"
(is (= {:result-code 1
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:violations [{:namespace 'sample.logic.foo
:dependency-namespace 'sample.controller.foo
:layer :logic
:dependency-layer :controller
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:level :error}]}
(api/analyze {:project-root (io/file (io/resource "with-violations"))
:config {:level :error}}))))

(testing "should return result code 0 when there is no violation with level equal to :error and the level in the config is :warning"
(is (= {:result-code 0
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:violations [{:namespace 'sample.logic.foo
:dependency-namespace 'sample.controller.foo
:layer :logic
:dependency-layer :controller
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:level :warning}]}
(api/analyze {:project-root (io/file (io/resource "with-violations"))
:config {:level :warning}}))))

(testing "should return result code 0 when there is no violation with level equal to :error and the level in the config is :info"
(is (= {:result-code 0
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:violations [{:namespace 'sample.logic.foo
:dependency-namespace 'sample.controller.foo
:layer :logic
:dependency-layer :controller
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:level :info}]}
(api/analyze {:project-root (io/file (io/resource "with-violations"))
:config {:level :info}})))))

(deftest analyze-project-with-custom-config
(testing "should fail when given a different configuration as a parameter"
(is (= {:result-code 1
Expand All @@ -56,7 +108,8 @@
:dependency-namespace 'sample.controller.foo
:layer :server
:dependency-layer :controller
:message "\"sample.server\" should not depend on \"sample.controller.foo\" (layer \":server\" on \":controller\")"}]}
:message "\"sample.server\" should not depend on \"sample.controller.foo\" (layer \":server\" on \":controller\")"
:level :error}]}
(api/analyze {:project-root (io/file (io/resource "without-violations"))
:config {:layers {:server {:namespaces #{'sample.server}
:accessed-by-layers #{}}
Expand All @@ -83,7 +136,8 @@
:dependency-namespace 'sample.controller.foo
:layer :server
:dependency-layer :controller
:message "\"sample.server\" should not depend on \"sample.controller.foo\" (layer \":server\" on \":controller\")"}]}
:message "\"sample.server\" should not depend on \"sample.controller.foo\" (layer \":server\" on \":controller\")"
:level :error}]}
(api/analyze {:project-root (io/file (io/resource "without-violations"))
:config {:source-paths #{}
:layers {:server {:namespaces #{'sample.server}
Expand All @@ -107,7 +161,8 @@
:dependency-namespace 'sample.controller.foo
:layer :logic
:dependency-layer :controller
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"}]}
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:level :error}]}
(api/analyze {:project-root (io/file (io/resource "with-violations"))
:namespaces #{'sample.logic.foo}})))))

Expand All @@ -125,7 +180,8 @@
:dependency-namespace 'sample.controller.foo
:layer :logic
:dependency-layer :controller
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"}]}
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:level :error}]}
(api/analyze {:project-root (io/file (io/resource "with-violations"))
:files #{(io/file (io/resource "with-violations/src/sample/logic/foo.clj"))}})))))

Expand All @@ -150,7 +206,8 @@
:dependency-namespace 'module2.logic.foo
:layer :module1-controller
:dependency-layer :module2-logic
:message "\"module1.controller.foo\" should not depend on \"module2.logic.foo\" (layer \":module1-controller\" on \":module2-logic\")"}]}
:message "\"module1.controller.foo\" should not depend on \"module2.logic.foo\" (layer \":module1-controller\" on \":module2-logic\")"
:level :error}]}
(api/analyze {:project-root (io/file (io/resource "with-violations-for-modular-structure"))})))))

(deftest analyze-project-with-violation-between-different-source-paths
Expand All @@ -167,7 +224,8 @@
:dependency-namespace 'sample.controller.foo
:layer :logic
:dependency-layer :controller
:message "\"sample.logic.foo-test\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"}]}
:message "\"sample.logic.foo-test\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:level :error}]}
(api/analyze {:project-root (io/file (io/resource "with-violations-between-different-source-paths"))
:config {:source-paths #{"src" "test"}}}))))

Expand All @@ -178,7 +236,8 @@
:dependency-namespace 'sample.controller.foo
:layer :logic
:dependency-layer :controller
:message "\"sample.logic.foo-test\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"}]}
:message "\"sample.logic.foo-test\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:level :error}]}
(api/analyze {:project-root (io/file (io/resource "with-violations-between-different-source-paths"))
:config {:source-paths #{}}}))))

Expand Down Expand Up @@ -221,7 +280,8 @@
:dependency-namespace 'sample.controller.foo
:layer :logic
:dependency-layer :controller
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"}]}
:message "\"sample.logic.foo\" should not depend on \"sample.controller.foo\" (layer \":logic\" on \":controller\")"
:level :error}]}
(api/analyze {:project-root (io/file (io/resource "with-violations-without-config"))
:config {:source-paths #{"src"}
:layers {:server {:namespaces #{'sample.server}
Expand Down