Welcome! Please see the About page for a little more info on how this works.

+1 vote
in Clojure by

Environment

  • Clojure 1.12.1 REPL with :local/root dependencies in deps.edn
  • Example: local.lib namespace at /Users/user/projects/repo/src/local/lib.clj

History

  1. Load namespace from :local/root dependency in REPL
  2. Verify function metadata:

    (meta #'local.lib/some-function)
    => {:arglists ([arg]),

     :line 42,
     :column 1,
     :file "/Users/user/projects/repo/src/local/lib.clj",
     :name some-function,
     :ns #object [clojure.lang.Namespace 0x123abc "local.lib"]}
    
  3. Call (clojure.repl/source-fn 'local.lib/some-function) returns nil

Expectation

source-fn should return the source code string for the symbol.

Actual

source-fn returns nil.

Evidence

(.getResourceAsStream (RT/baseLoader) "/Users/user/projects/repo/src/local/lib.clj") returns nil because RT/baseLoader cannot access local filesystem paths, only classpath JAR resources.

Impact

Breaks REPL-driven development workflow for :local/root dependencies. Creates an inconsistent experience between JAR and local dependencies.

ago by
How exactly did you "Load namespace from :local/root dependency in REPL"?

If I just eval the file via Calva into the REPL, I can't get source. If I explicitly require the ns, I can get source.
ago by
Hi. I responded to Alex below with more details. I made a repro here https://github.com/kennyjwilli/source-fn-repro.

1 Answer

0 votes
by
edited by

This works for me locally but I'm assuming things you haven't provided. What's the deps.edn of repo? what's the deps.edn/command line of the REPL? Did you (require 'local.lib)? (It is necessary to load the namespace before you call source-fn.)

  • My repo deps.edn: {:paths ["src"]}
  • My command line: clj -Sdeps '{:deps {local/repo {:local/root "repo"}}}'

    Clojure 1.12.1
    user=> (require 'local.lib)
    nil
    user=> (meta #'local.lib/some-function)
    {:arglists ([]), :line 3, :column 1, :file "local/lib.clj", :name some-function, :ns #object[clojure.lang.Namespace 0x14f3c6fc "local.lib"]}
    user=> (clojure.repl/source-fn 'local.lib/some-function)
    "(defn some-function [] \"hi\")"

"RT/baseLoader cannot access local filesystem paths, only classpath JAR resources." is incorrect. The file system resource path you care about here is NOT an absolute path, it is: (.getResourceAsStream (clojure.lang.RT/baseLoader) "local/lib.clj") (which is subsequently a path found via the classpath roots, which may be either in directories or jars:

user=> (:file (meta #'local.lib/some-function))
"local/lib.clj"
ago by
Great question! It took me a bit to recreate this issue, but I can reproduce it consistently. Here's how to set it up:

**Project structure:**
```
.
├── code
│   └── lib1
│       ├── deps.edn
│       └── src
│           └── foo
│               ├── a.clj
│               └── b.clj
└── deps.edn
```

**Root deps.edn file:**
```clojure
{:paths   []
 :aliases {:lib1 {:extra-deps {lib1/lib1 {:local/root "code/lib1"}}}}}
```

**code/lib1/src/foo/a.clj:**
```clojure
(ns foo.a)
(defn func-a [] "from a")
```

**code/lib1/src/foo/b.clj:**
```clojure
(ns foo.b
  (:require [foo.a :as a]))
(defn func-b [] (str "from b, calling: " (a/func-a)))
```

**Steps to reproduce:**

1. Start a REPL from the root directory: `clj -A:lib1`

2. In the REPL, run these commands:
```clojure
Clojure 1.12.1
user=> (load-file "code/lib1/src/foo/b.clj")
#'foo.b/func-b

user=> (meta #'foo.b/func-b)
{:arglists ([]), :line 4, :column 1, :file "/Users/kenny/github/source-fn-repro/code/lib1/src/foo/b.clj", :name func-b, :ns #object[clojure.lang.Namespace 0x21325036 "foo.b"]}

user=> (meta #'foo.a/func-a)
{:arglists ([]), :line 2, :column 1, :file "foo/a.clj", :name func-a, :ns #object[clojure.lang.Namespace 0x6de30571 "foo.a"]}
```

**The issue:** Notice that `func-b` shows the full file path, but `func-a` only shows the relative path `"foo/a.clj"` without the complete directory structure.

Here's the repro code: https://github.com/kennyjwilli/source-fn-repro
ago by
load-file is the problem here. If you use require, it works as expected:

(!2002)-> clj -A:lib1
Clojure 1.12.1
user=> (require 'foo.b)
nil
user=> (meta #'foo.b/func-b)
{:arglists ([]), :line 4, :column 1, :file "foo/b.clj", :name func-b, :ns #object[clojure.lang.Namespace 0xc446b14 "foo.b"]}
user=> (meta #'foo.a/func-a)
{:arglists ([]), :line 2, :column 1, :file "foo/a.clj", :name func-a, :ns #object[clojure.lang.Namespace 0x7e46d648 "foo.a"]}
user=> (source foo.b/func-b)
(defn func-b [] (str "from b, calling: " (a/func-a)))
nil
user=> (source foo.a/func-a)
(defn func-a [] "from a")
nil
user=>
ago by
load-file is a low-level function that reads a file and evaluating the forms vs load (or require) which is taking a classpath-centric approach. Because load-file is file-based, it must use the full absolute or relative path to the file as meta (there is no classpath-centric "relative").

`source` is classpath-relative, "This requires that the symbol resolve to a Var defined in a namespace for which the .clj is in the classpath." per the docstring. Loading via load-file creates vars with metadata incompatible with how source works.

I guess backing up - why are you using load-file and not load relative to the classpath?
ago by
Ah, interesting. Thanks for the insight!

So I’m actually not using it directly, which is why this was a bit tricky to repro, haha. I’m just using Cursive’s load file REPL shortcut, which, I suppose, must be using load-file under the hood? Not sure. Perhaps the right follow-up here is why is Cursive using load-file versus require. I can post a question to #cursive.
...