Giter Club home page Giter Club logo

grasp's Introduction

grasp

Grep Clojure code using clojure.spec regexes. Inspired by grape.

Why

This tool allows you to find patterns in Clojure code. I use it as a research tool for sci, clj-kondo or Clojure tickets.

Dependency

deps.edn

io.github.borkdude/grasp {:mvn/version "0.1.4"}

API

The grasp.api namespace currently exposes:

  • (grasp path-or-paths spec): returns matched sexprs in path or paths for spec. Accept source file, directory, jar file or classpath as string as well as a collection of strings for passing multiple paths. In case of a directory, it will be scanned recursively for source files ending with .clj, .cljs or .cljc.
  • (grasp-string string spec): returns matched sexprs in string for spec.
  • resolve-symbol: returns the resolved symbol for a symbol, taking into account aliases and refers. You can also use rsym to create a spec that matches a fully-qualified, resolved symbol.
  • unwrap: see Finding keywords.
  • cat, or, seq, vec: see Convenience macros.
  • *, ?, +: aliases for (s/* any?), etc.

Status

Very alpha. API will almost certainly change.

Example usage

Assuming you have the following requires:

(require '[clojure.java.io :as io]
         '[clojure.pprint :as pprint]
         '[clojure.string :as str]
         '[clojure.spec.alpha :as s]
         '[grasp.api :as g])

Find reify usages

Find reify usage with more than one interface:

(def clojure-core (slurp (io/resource "clojure/core.clj")))

(s/def ::clause (s/cat :sym symbol? :lists (s/+ list?)))

(s/def ::reify
  (s/cat :reify #{'reify}
         :clauses (s/cat :clause ::clause :clauses (s/+ ::clause))))

(def matches (g/grasp-string clojure-core ::reify))

(doseq [m matches]
  (prn (meta m))
  (pprint/pprint m)
  (println))

This outputs:

{:line 6974, :column 5, :end-line 6988, :end-column 56}
(reify
 clojure.lang.IDeref
 (deref [_] (deref-future fut))
 clojure.lang.IBlockingDeref
 (deref
  [_ timeout-ms timeout-val]
  (deref-future fut timeout-ms timeout-val))
 ...)

{:line 7107, :column 5, :end-line 7125, :end-column 16}
(reify
 clojure.lang.IDeref
 ...)

(output abbreviated for readability)

Find usages based on resolved symbol

Find all usages of clojure.set/difference:

(defn table-row [sexpr]
  (-> (meta sexpr)
      (select-keys [:uri :line :column])
      (assoc :sexpr sexpr)))

(->>
   (g/grasp "/Users/borkdude/git/clojure/src"
            ;; Alt 1: using rsym:
            (g/rsym 'clojure.set/difference)
            ;; Alt 2: do it manually:
            #_(fn [sym]
              (when (symbol? sym)
                (= 'clojure.set/difference (g/resolve-symbol sym)))))
   (map table-row)
   pprint/print-table)

This outputs:

|                                                         :uri | :line | :column |         :sexpr |
|--------------------------------------------------------------+-------+---------+----------------|
|     file:/Users/borkdude/git/clojure/src/clj/clojure/set.clj |    49 |       7 |     difference |
|     file:/Users/borkdude/git/clojure/src/clj/clojure/set.clj |    62 |      14 |     difference |
|     file:/Users/borkdude/git/clojure/src/clj/clojure/set.clj |   172 |       2 |     difference |
|    file:/Users/borkdude/git/clojure/src/clj/clojure/data.clj |   112 |      19 | set/difference |
|    file:/Users/borkdude/git/clojure/src/clj/clojure/data.clj |   113 |      19 | set/difference |
| file:/Users/borkdude/git/clojure/src/clj/clojure/reflect.clj |   107 |      37 | set/difference |

Find a function call

Find all calls to clojure.core/map that take 1 argument:

(g/grasp-string "(comment (map identity))" (g/seq (g/rsym 'clojure.core/map) any?))
; => [(map identity)]

Grasp a classpath

Grasp the entire classpath for usage of frequencies:

(->> (g/grasp (System/getProperty "java.class.path") #{'frequencies})
     (take 2)
     (map (comp #(select-keys % [:uri :line]) meta)))

Output:

({:uri "file:/Users/borkdude/.gitlibs/libs/borkdude/sci/cb96d7fb2a37a7c21c78fc145948d6867c30936a/src/sci/impl/namespaces.cljc", :line 815}
 {:uri "file:/Users/borkdude/.gitlibs/libs/borkdude/sci/cb96d7fb2a37a7c21c78fc145948d6867c30936a/src/sci/impl/namespaces.cljc", :line 815})

Finding keywords

When searching for keywords you will run into the problem that they do not have location information because they can't carry metadata. To solve this problem, grasp lets you wrap non-metadata supporting forms in a container. Grasp exposes the unwrap function to get hold of the form, while you can access the location of that form using the container's metadata. Say we would like to find all occurrences of :my.cljs.app.subs/my-data in this example:

/tmp/code.clj:

(ns my.cljs.app.views
  (:require [my.cljs.app.subs :as subs]
            [re-frame.core :refer [subscribe]]))

(subscribe [::subs/my-data])
(subscribe [:my.cljs.app.subs/my-data])

We can find them like this:

(s/def ::subscription (fn [x] (= :my.cljs.app.subs/my-data (unwrap x))))

(def matches
  (grasp "/tmp/code.clj" ::subscription {:wrap true}))

(run! prn (map meta matches))

Note that you explicitly have to provide :wrap true to make grasp wrap keywords.

The output:

{:line 5, :column 13, :end-line 5, :end-column 27, :uri "file:/tmp/code.clj"}
{:line 6, :column 13, :end-line 6, :end-column 38, :uri "file:/tmp/code.clj"}

Keep-fn

Grasp supports a custom :keep-fn, the function which decides whether to collect a matched result. The default :keep-fn is:

(defn default-keep-fn
  [{:keys [spec expr uri]}]
  (when (s/valid? spec expr)
    (impl/with-uri expr uri)))

When a spec result is valid, then the URI is attached to the result's metadata and kept.

In a custom :keep-fn you are able to call s/conform and keep that result around:

(defn keep-fn [{:keys [spec expr uri]}]
  (let [conformed (s/conform spec expr)]
    (when-not (s/invalid? conformed)
      {:var-name (grasp/resolve-symbol (second expr))
       :expr expr
       :uri uri})))

Now the result of g/grasp will be a seq of maps instead of expressions and you can do whatever you want with it.

Matching on source string

Using the option :source true, grasp will attach the source string as metadata on parsed s-expressions. This can be used to match on things like function literals like #(foo %) or keywords like ::foo. For example: we can grasp for function literals that have more than one argument:

(s/def ::fn-literal
  (fn [x] (and (seq? x) (= 'fn* (first x)) (> (count (second x)) 1)
               (some-> x meta :source (str/starts-with? "#("))))))

(def match (first (g/grasp-string "#(+ % %2)" ::fn-literal {:source true})))

(prn [match (meta match)])

Output:

[(fn* [%1 %2] (+ %1 %2)) {:source "#(+ % %2)", :line 1, :column 1, :end-line 1, :end-column 10}]

More examples

More examples in examples.

Convenience macros

Grasp exposes the cat, seq, vec and or convenience macros.

All of these macros support passing in a single quoted value for matching a literal thing 'foo for matching that symbol instead of #{'foo}. Additionally, they let you write specs without names for each parsed item: (g/cat 'foo int?) instead of (s/cat :s #{'foo} :i int?). The seq and vec macros are like the cat macro but additionally check for seq? and vector? respectively.

Binary

A CLI binary can be obtained from Github releases.

It can be invoked like this:

$ ./grasp ~/git/spec.alpha/src -e "(set-opts! {:wrap true}) (fn [k] (= :clojure.spec.alpha/invalid (unwrap k)))" | grep file | wc -l
      68

The binary supports the following options:

-p, --path: path
-e, --expr: spec from expr
-f, --file: spec from file

The path and spec may also be provided without flags, like grasp <path> <spec>. Use - for grasping from stdin.

The evaluated code from -e or -f may return a spec (or spec keyword) or call set-opts! with a map that contains :spec and other options. E.g.:

(require '[clojure.spec.alpha :as s])
(require '[grasp.api :as g])

(s/def ::spec (fn [x] (= :clojure.spec.alpha/invalid (g/unwrap x))))

(g/set-opts! {:spec ::spec :wrap true})

If nil is returned from the evaluated code and set-opts! wasn't called, the CLI assumes that code will handle the results and no printing will be done. These programs may call g/grasp and pass g/*path* which contains the path that was passed to the CLI.

Full example:

fn_literal.clj:

(require '[clojure.pprint :as pprint]
         '[clojure.spec.alpha :as s]
         '[clojure.string :as str]
         '[grasp.api :as g])

(s/def ::fn-literal
  (fn [x] (and (seq? x) (= 'fn* (first x)) (> (count (second x)) 1)
               (some-> x meta :source (str/starts-with? "#(")))))

(let [matches (g/grasp g/*path* ::fn-literal {:source true})
      rows (map (fn [match]
                  (let [m (meta match)]
                    {:source (:source m)
                     :match match}))
                matches)]
  (pprint/print-table rows))
$ grasp - fn_literal.clj <<< "#(foo %1 %2)"

|  :uri | :line |      :source |                    :match |
|-------+-------+--------------+---------------------------|
| stdin |     1 | #(foo %1 %2) | (fn* [%1 %2] (foo %1 %2)) |

Pattern matching

The matched s-expressions can be conformed and then pattern-matched using libraries like meander.

Revisiting the ::reify spec which finds reify usage with more than one interface:

(s/def ::clause (s/cat :sym symbol? :lists (s/+ list?)))

(s/def ::reify
  (s/cat :reify #{'reify}
         :clauses (s/cat :clause ::clause :clauses (s/+ ::clause))))

(def clojure-core (slurp (io/resource "clojure/core.clj")))

(def matches (g/grasp-string clojure-core ::reify))

(def conformed (map #(s/conform ::reify %) matches))
(require '[matchete.core :as mc])

(def pattern
  {:clauses
   {:clause {:sym '!interface}
    :clauses (mc/each {:sym '!interface})}})

(first (mc/matches pattern (first conformed)))

Returns:

{!interface [clojure.lang.IDeref clojure.lang.IBlockingDeref clojure.lang.IPending java.util.concurrent.Future]}
(require '[meander.epsilon :as m])

(m/find
  (first conformed)
  {:clauses {:clause {:sym !interface} :clauses [{:sym !interface} ...]}}
  !interface)

Returns:

[clojure.lang.IDeref clojure.lang.IBlockingDeref clojure.lang.IPending java.util.concurrent.Future]

Build

Run script/compile to compile the grasp binary using GraalVM

License

Copyright © 2020 Michiel Borkent

Distributed under the EPL License. See LICENSE.

grasp's People

Contributors

arohner avatar borkdude avatar holyjak avatar laurio avatar mk avatar philomates avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

grasp's Issues

Analyzing symbols with refer all in namespace, assume current workspace incorrectly

When analyzing a file such which has a refer all (which is known to be bad practice) the analyzer
does not add the extra information to possible unresolved symbols that can match the symbol

for example in:

(ns samples.web
  (:require [compojure.core :refer :all]
            [compojure.route :as route]))
(defroutes app
           (GET "/" [] "<h1>Hello World</h1>")
           (route/not-found "<h1>Page not found</h1>"))

We receive a symbol list of

samples.web/defroutes
samples.web/GET
compojure.route/not-found

The :refer :all in clojure matches the symbol of GET with compojure.core/GET.
but in grasp it does not match anything and defaults to the current namespace

I would suggest adding all possible matches shown below and adding a metadata remark on the symbol:

compujure.core/GET
samples.web/GET

Grasp doesn't seem to descend into macro forms

I'm looking for all strings in our codebase at Metabase that need translations. To this end i'm doing

(s/def ::translate (s/and
                     (complement vector?)
                     (s/cat :translate-symbol (fn [x]
                                               (and (symbol? x)
                                                    (#{"trs" "deferred-trs"
                                                       "tru" "deferred-tru"}
                                                      (name x))))
                            :args (s/+ any?))))

(I only just now realized I can resolve symbols for smarter matching but not relevant here). This spec works great and finds almost all usages. However I've found one that it does not find:

(defmacro ^:private deffingerprinter
  [field-type transducer]
  {:pre [(keyword? field-type)]}
  (let [field-type [field-type :Semantic/* :Relation/*]]
    `(defmethod fingerprinter ~field-type
       [field#]
       (with-error-handling
         (with-global-fingerprinter
           (redux/post-complete
            ~transducer
            (fn [fingerprint#]
              {:type {~(first field-type) fingerprint#}})))
         (trs "Error generating fingerprint for {0}" (sync-util/name-for-logging field#))))))

I'm going to try to come up with a minimal reproduction because there's a lot going on.

  (g/grasp "/Users/dan/projects/work/metabase/src/metabase/sync/analyze/fingerprint/fingerprinters.clj"
           ::translate)
;; misses the example above
[(deferred-trs "Error reducing {0}" (name k))]

Note I'm not expecting to find the usage at macro invocations and call sites, just in the literal form here which I would expect it to be able to descend into and match on the trs.

Allow to record more information for matches

For a grasp analysis I did on one of our projects, I was interested in finding all usages of :pre and :post maps in defns. For this, I thought the fully-qualified var name would make a better identifier than the plain name and the file. But since grasp/resolve-symbol depends on the *ctx* being bound, I believe we need to add a way to run a user-provided function during analysis to enable this.

Another (smaller) benefit of this could be that we can avoid needing to run conform twice like in the arg-vec example.

Should not fail when classpath dir doesn't exist

Trying this on our project with

(g/grasp (System/getProperty "java.class.path") #{:pre :post})

Fails with

Unhandled java.io.FileNotFoundException
   /Users/mk/.gitlibs/libs/applied-science/waqi/a738123afae999656d7a0ff77edb431c97ebb341/resources
   (No such file or directory)

      FileInputStream.java:   -2  java.io.FileInputStream/open0
      FileInputStream.java:  216  java.io.FileInputStream/open
      FileInputStream.java:  157  java.io.FileInputStream/<init>
                    io.clj:  229  clojure.java.io/fn
                    io.clj:  229  clojure.java.io/fn
                    io.clj:   69  clojure.java.io/fn/G
                    io.clj:  165  clojure.java.io/fn
                    io.clj:  165  clojure.java.io/fn
                    io.clj:   69  clojure.java.io/fn/G
                    io.clj:  102  clojure.java.io/reader
                    io.clj:   86  clojure.java.io/reader
               RestFn.java:  410  clojure.lang.RestFn/invoke
                  AFn.java:  154  clojure.lang.AFn/applyToHelper
               RestFn.java:  132  clojure.lang.RestFn/applyTo
                  core.clj:  669  clojure.core/apply
                  core.clj: 7009  clojure.core/slurp
                  core.clj: 7009  clojure.core/slurp
               RestFn.java:  410  clojure.lang.RestFn/invoke
                  impl.clj:  251  grasp.impl/grasp
                  impl.clj:  235  grasp.impl/grasp
                  impl.clj:  239  grasp.impl/grasp/fn
                  core.clj: 2770  clojure.core/map/fn
              LazySeq.java:   42  clojure.lang.LazySeq/sval
              LazySeq.java:   51  clojure.lang.LazySeq/seq
                   RT.java:  535  clojure.lang.RT/seq
                  core.clj:  139  clojure.core/seq
                  core.clj:  662  clojure.core/apply
                  core.clj: 2800  clojure.core/mapcat
                  core.clj: 2800  clojure.core/mapcat
               RestFn.java:  423  clojure.lang.RestFn/invoke
                  impl.clj:  239  grasp.impl/grasp
                  impl.clj:  235  grasp.impl/grasp
                   api.clj:    9  grasp.api/grasp
                   api.clj:    6  grasp.api/grasp
                   api.clj:    7  grasp.api/grasp
                   api.clj:    6  grasp.api/grasp
                      REPL:   14  ductile.insights.clojure-pre-post/eval75485
                      REPL:   14  ductile.insights.clojure-pre-post/eval75485
             Compiler.java: 7194  clojure.lang.Compiler/eval
             Compiler.java: 7149  clojure.lang.Compiler/eval
                  core.clj: 3215  clojure.core/eval
                  core.clj: 3211  clojure.core/eval
    interruptible_eval.clj:   87  nrepl.middleware.interruptible-eval/evaluate/fn/fn
                  AFn.java:  152  clojure.lang.AFn/applyToHelper
                  AFn.java:  144  clojure.lang.AFn/applyTo
                  core.clj:  667  clojure.core/apply
                  core.clj: 1990  clojure.core/with-bindings*
                  core.clj: 1990  clojure.core/with-bindings*
               RestFn.java:  425  clojure.lang.RestFn/invoke
    interruptible_eval.clj:   87  nrepl.middleware.interruptible-eval/evaluate/fn
                  main.clj:  437  clojure.main/repl/read-eval-print/fn
                  main.clj:  437  clojure.main/repl/read-eval-print
                  main.clj:  458  clojure.main/repl/fn
                  main.clj:  458  clojure.main/repl
                  main.clj:  368  clojure.main/repl
               RestFn.java: 1523  clojure.lang.RestFn/invoke
    interruptible_eval.clj:   84  nrepl.middleware.interruptible-eval/evaluate
    interruptible_eval.clj:   56  nrepl.middleware.interruptible-eval/evaluate
    interruptible_eval.clj:  152  nrepl.middleware.interruptible-eval/interruptible-eval/fn/fn
                  AFn.java:   22  clojure.lang.AFn/run
               session.clj:  218  nrepl.middleware.session/session-exec/main-loop/fn
               session.clj:  217  nrepl.middleware.session/session-exec/main-loop
                  AFn.java:   22  clojure.lang.AFn/run
               Thread.java:  833  java.lang.Thread/run

Creating that dir makes it fail on the next path. I think it should not assume that every dir on the classpath needs to exist.

Cyclic load dependency in babashka

When using grasp with babashka, requiring the grasp.api namespace fails with the message Cyclic load dependency: grasp.api->[ grasp.impl ]->[ grasp.impl ] [at grasp/impl.clj]

I'm not sure why grasp.impl requires itself. I get other errors when I remove it, which I'll open other issues for when I investigate further.

Minimal reproduction

$ echo '{:deps {io.github.borkdude/grasp {:mvn/version "0.0.3"} org.babashka/spec.alpha {:git/url "https://github.com/babashka/spec.alpha" :git/sha "8df0712896f596680da7a32ae44bb000b7e45e68"}}}' > bb.edn
$ bb
Babashka v0.9.161 REPL.
Use :repl/quit or :repl/exit to quit the REPL.
Clojure rocks, Bash reaches.

user=> (require '[grasp.api :as g])
Cyclic load dependency: grasp.api->[ grasp.impl ]->[ grasp.impl ] [at grasp/impl.clj]
user=>

False positive when searching for `(def <symbol?>)`

$ script/run-cli $(clojure -Spath) -e "(s/cat :_ #{'def} :_ symbol?)"
...
jar:file:/Users/borkdude/.m2/repository/borkdude/edamame/0.0.11-alpha.15/edamame-0.0.11-alpha.15.jar!/edamame/impl/parser.cljc:94:1
(def non-match ::nil)

Add test

Test all examples from README and rich comment form at the bottom of api.clj.

Speed up analysis

Two ideas:

  • Allow processing of files in parallel (see for the same trick the clj-kondo code)
  • Allow predicate hook on file and/or parsed sexpr to decide if it's worth to analyze at all. E.g. (not (str/includes? (pr-str form) "assoc"))

Add conveniences for searching for function calls?

Add a convenience macro list similar to cat but checking that it is a list and something that makes it easy to match on a call to a particular function. What I had to do, to find where my-fun is called with three arguments (b/c we are deprecating the arity so I wanted to know where we use it) was:

(s/def ::my-fn (s/and symbol? (comp #{'my.ns/my-fn} g/resolve-symbol)))
(g/grasp "./" (g/cat ::my-fn any? any?  any?)

That is obvioulsy quite neat already. But I could have imagined st. like:

(g/grasp "./" (g/list (rsym my.ns/my-fn) any? any?  any?)

where rsym is a macro that expands into (s/and symbol? (comp #{<the symbol>} g/resolve-symbol))).

I would be happy to contribute code for this.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.