Skip to content
This repository has been archived by the owner on Oct 2, 2020. It is now read-only.

Removes import only used in type hint #72

Closed
seancorfield opened this issue Mar 23, 2014 · 17 comments
Closed

Removes import only used in type hint #72

seancorfield opened this issue Mar 23, 2014 · 17 comments

Comments

@seancorfield
Copy link
Contributor

We import several classes only to use them in type hints. Slamhound removes the import for them.

Example:

(ns worldsingles.newrelic
  (:require [worldsingles.environment :as env]
            [worldsingles.logging :as log])
  (:import (com.newrelic.metrics.publish Agent AgentFactory Runner)
           (com.newrelic.metrics.publish.configuration ConfigurationException)
           (com.newrelic.metrics.publish.processors EpochCounter Processor)))

...
        (proxy [Agent] ;; extends Agent
            [agent-name agent-version] ;; super (constructor)

          (getComponentHumanLabel ^String [] name) ;; displays in New Relic

          (pollCycle [] ;; called every cycle by New Relic process
            (probe (fn [^String label ^String scale type value]
                     (.reportMetric ^Agent this label scale
                                    (if (= type :value)
                                      value
                                      (.process ^Processor (get-rate label scale) value)))))))))))

Agent is retained - used in the proxy call - but Processor is removed.

We ran into this with other New Relic code where we import com.newrelic.api.agent.Trace and use ^{Trace {}} as metadata to create Java annotations. Again the import is removed.

@guns
Copy link
Collaborator

guns commented Mar 23, 2014

I've identified the problem. Consider the following ns:

(ns slam.metadata-problem)

(def example-pattern
  #"foo")

(defn example []
  (.pattern ^Pattern example-pattern))

Running Slamhound on this buffer works as expected:

(ns slam.metadata-problem
  (:import (java.util.regex Pattern)))

(def example-pattern
  #"foo")

(defn example []
  (.pattern ^Pattern example-pattern))

However, if we change example-pattern to a function:

(ns slam.metadata-problem)

(defn example-pattern []
  #"foo")

(defn example []
  (.pattern ^Pattern (example-pattern)))

Slamhound does not detect the ^Pattern tag.

This clearly has something to do with metadata interaction with lists. While it's not yet clear whether we can really do anything about this on our end without hijacking the reader, there is an obvious solution that works in both this buffer and your example:

(ns slam.metadata-problem)

(defn ^Pattern example-pattern []
  #"foo")

(defn example []
  (.pattern (example-pattern)))

Tagging the var directly avoids reflection and also allows Slamhound to find the missing reference to Pattern.

I'd like to keep this open until I can ascertain whether or not there is a nice solution for this.

Thanks again for all the feedback! This is really great.

@seancorfield
Copy link
Contributor Author

Interesting. So using a local let binding for the result of (get-rate label scale) should solve that? Let me try...

@seancorfield
Copy link
Contributor Author

Yup, that works. Cool. This is also most likely the cause of Slamhound removing the com.newrelic.api.agent.Trace import since Trace is only used in metadata: ^{Trace {}}? I can use the fully-qualified type there instead of importing it I suspect, which should make our entire codebase Slamhound compatible.

@guns
Copy link
Collaborator

guns commented Mar 23, 2014

Well, it looks like literal metadata maps don't work either way. They do work on symbols on defn and defmacro however:

(defmacro ^{:require [Pattern]} make-pattern […])

This will require a deeper dive into the reader/compiler.

@seancorfield
Copy link
Contributor Author

Whilst this issue still persists, we were able to make our code base "Slamhound-compatible" and run Slamhound across every source namespace. We haven't run it across our test namespaces yet, but probably will (although those are much simpler ns declarations). We really like the cleanups!

@guns
Copy link
Collaborator

guns commented Mar 28, 2014

That is very cool. Thank you for sticking through it and reporting issues.

I've always wanted a tool like Slamhound in every language I've worked in¹, so I am very eager to see this project become a standard tool among Clojure developers.

As for this metadata issue, I hope to have it fixed over the weekend.

¹ I am vaguely aware that "IDE"s may have had this feature for some time now

guns added a commit that referenced this issue Mar 30, 2014
In order to accurately test Slamhound's interaction with files, we must
test the output of the entire process:

    - Open a reader on a file
    - Receive Clojure forms from reader (with reader macros expanded)
    - Reconstruct namespace
    - Write the new namespace back to the file, but copy everything else
      back verbatim

Many tests within Slamhound use (StringReader. (str '(literal forms)))
in order to avoid the drudgery of doing a manual pr-str.

Unfortunately, (str '[^{:my-metadata "value"} x]) outputs: "[x]", so any
metadata on the forms is irretrievably lost.

Since the interaction of Slamhound and metadata on forms is non-obvious,
I think writing our integration tests as comparisons of input and output
_files_ will help us detect these issues sooner in the future.¹

I have placed with-transform-test and with-tempfile into a common
testing namespace so that they can be used across all test files.

RE: the name `with-transform-test`:

    - I read it as "With transform, test …"
    - Many editors commonly treat ^with-.* as a specially indented form
    - It's awkward, so if a better name is found, let's use it

¹ See issue #72, and commit 54cba5c for
  past issues with metadata
guns added a commit that referenced this issue Mar 30, 2014
Commit 54cba5c introduced a call to clojure.walk/prewalk to dequalify
vars that were qualified to the current *ns*. A side effect of this is
that metadata on composite forms was lost because clojure.walk/walk does
not preserve the metadata on these forms.

To address this, we define our own version of prewalk that properly
preserves metadata.

Addresses #72
@guns
Copy link
Collaborator

guns commented Mar 30, 2014

I think I have discovered the issue. Commit 54cba5c introduced the use of clojure.walk/prewalk to munge some symbols that had been improperly qualified by the reader.

It turns out that clojure.walk/walk does not preserve metadata on compound forms, so the temporary namespace that gets evaluated during regrow never sees this metadata.

Adding a custom walk function that does preserve this metadata appears to fix the issue. Testing is much appreciated.

@seancorfield
Copy link
Contributor Author

That doesn't seem to fix it, I'm afraid. With this import:

(:import (com.newrelic.api.agent Trace))

and this usage:

(deftype NR []
  INR
  (^{Trace {}} do_search
    [_ search-data order-by view-as page page-size skip-exact reverse-search search-rule]
    (do-search* search-data order-by view-as page page-size skip-exact reverse-search search-rule)))

Slamhound still removes the import.

@guns
Copy link
Collaborator

guns commented Mar 30, 2014

It does work now for tagging lists and other collections:

(.method ^AClass (make-object))

But yes, it doesn't work on the symbols of protocol implementations… thank you for testing. I'm looking into it now.

@guns
Copy link
Collaborator

guns commented Mar 30, 2014

It appears that symbols in the metadata of method implementations are only evaluated as Class names then they are attached to the :tag key:

(defprotocol Foo
  (foo [this]))

;; The following two expressions throw with
;; "Unable to resolve classname: ANonExistantClass"
(deftype AFoo []
  Foo
  (^{:tag ANonExistantClass} foo [this]))

(deftype BFoo []
  Foo
  (^ANonExistantClass foo [this]))

However, in any other position they remain as unevaluated symbols:

;; These compile with no errors!

(deftype AFoo []
  Foo
  (^{:anything ANonExistantClass} foo [this]))

(deftype BFoo []
  Foo
  (^{ANonExistantClass []} foo [this]))

I haven't finished following the macroexpansion, but this appears to be the issue. If your use of ^{Trace {}} really does have an effect on the method implementation, I suspect whatever is inspecting the metadata of the method may be looking for the Symbol 'Trace, and not the Class com.newrelic.api.agent.Trace.

Or maybe not; I'd love to have your feedback.

@seancorfield
Copy link
Contributor Author

Sorry, that was just the easiest namespace to test against. I can confirm it now works as expected on the original namespace that I raised this issue for - Processor is retained now, without the let.

In the ^{Trace {}} case, that's a Java annotation in Clojure - @Trace in Java:

http://corfield.org/blog/post.cfm/instrumenting-clojure-for-new-relic-monitoring

So it's affecting something in the generated bytecode but I don't know enough about how Java annotations work to provide more details.

@guns
Copy link
Collaborator

guns commented Mar 30, 2014

Ah, this bit about annotations is new to me. I'll follow the directions in your blog post and take a closer look.

@guns
Copy link
Collaborator

guns commented Mar 30, 2014

Here are some notes so far.

(defn get-method-annotations
  "Get the annotations on a method.
   cf. https://gist.github.com/richhickey/377213"
  [^Class cls method-name]
  (seq (.getAnnotations (.getMethod cls method-name nil))))

(defprotocol IFoo
  (foo [this]))

(deftype Foo []
  IFoo
  (^{Retention {}} foo [this]))

(get-method-annotations Foo "foo") ; => nil

;;
;; Using the full class name works.
;;

(deftype Bar []
  IFoo
  (^{java.lang.annotation.Retention {}} foo [this]))

(get-method-annotations Bar "foo") ; => (#<$Proxy1 @java.lang.annotation.Retention()>)

;;
;; But so does a simple symbol if it resolves to a class.
;;

(import 'java.lang.annotation.Retention)

(deftype Baz []
  IFoo
  (^{Retention {}} foo [this]))

(get-method-annotations Baz "foo") ; => (#<$Proxy1 @java.lang.annotation.Retention()>)

So the issue is that ^{AClassSymbol {}} is interpreted as metadata of a symbol -> value if AClassSymbol does not resolve to class, and is interpreted as an annotation if it does resolve to class.

I think we can work around this.

guns added a commit that referenced this issue Mar 31, 2014
Rich Hickey introduced support for Java annotations on definterface,
deftype, and defrecord types on 23 April, 2010¹.

Unfortunately, since the Clojure compiler allows the use of unquoted
symbols in metadata, annotations of the form

    ^{ClassSymbol value}

are only interpreted as annotations when ClassSymbol resolves to an
imported class.

Since a compiler error is not generated in either case, we must walk any
forms that may contain annotations and force the compiler to resolve
them.

We already walk the body once to dequalify erroneously qualified
symbols, so this patch hooks into this traversal to collect a set of
class symbols that may be interpreted as annotations.

This set of symbols is appended to the candidate body so that they
become visible to the compiler during check-for-failure.

It is possible that metadata inside of a definterface/deftype/defrecord
form of the form ^{CapitalizedSymbol value} may be erroneously
interpreted as an annotation instead of just metadata of Symbol -> Any.

This is impossible to determine without user intervention, but since
most metadata keys are keywords, I think interpreting these entries as
annotations is a good bet.

Addresses #72

¹ https://groups.google.com/forum/#!topic/clojure/0hKOFQXAwRc
@guns
Copy link
Collaborator

guns commented Mar 31, 2014

Okay, I've pushed a possible solution to the branch java-annotations, but I'm going to have to get the go-ahead before merging.

@technomancy

The rationale behind the implementation is in the commit message, but this is the way it works:

  • While walking the body, look for definterface/deftype/defrecord forms
  • Instead of trying to parse these forms, walk through them and extract all metadata keys on symbols that are:
    1. Symbols
    2. Capitalized
    3. Do not contain . or /
  • Concat these symbols to the candidate body. e.g.
'((deftype AFoo [^{WebServiceRefs []} field]
    ^{SupportedOptions []}
    Foo
    (^{Retention RetentionPolicy/RUNTIME} foo [this])))

After munging it becomes:

'((deftype AFoo [^{WebServiceRefs []} field]
    ^{SupportedOptions []}
    Foo
    (^{Retention RetentionPolicy/RUNTIME} foo [this]))
  WebServiceRefs
  SupportedOptions
  Retention)
;; Note that RetentionPolicy is resolved once the compiler recognizes
;; the entry is an annotation

The appended symbols are effectively NOPs, but they do force the compiler to resolve them in the usual way. Besides being a bit sneaky, I currently do not see a downside to this approach.

I initially thought that this would be a good place to advise the user to use ^{:requires […]}, but it turns out that attaching the dummy metadata to the deftype symbol does nothing:

;; This does not work!
(deftype ^{:requires [WebServiceRefs SupportedOptions Retention]}
  AFoo []
  …)

Although this feature is a bit arcane, and the details of the implementation are a bit repulsive, I still think that this is a good inclusion if it precludes the existence of another future Twitter user proclaiming that Slamhound is "not ready for prime time".

@technomancy
Copy link
Owner

It's really unfortunate that the compiler is so sloppy here, but I guess we have to work with what we've got. I don't have any objection to merging this; thanks for putting this together. I don't use deftype, defrecord, or definterface myself.

@guns
Copy link
Collaborator

guns commented Apr 1, 2014

@seancorfield

Whenever you have a chance, I'd love to know how this branch works for you.

@seancorfield
Copy link
Contributor Author

Just verified this works locally for our annotations. Thank you!!

@guns guns closed this as completed in f8456cd Apr 1, 2014
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants