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

+4 votes
in Compiler by
edited by

I was recently involved in a project to upgrade CongoMongo to the latest version of the MongoDB Java library, and progress was slowed because there was no way to identify where CongoMongo was making calls to deprecated Java APIs.

I would like if Clojure could emit a warning from the Compiler when a call is to made to a Java method that has been marked with the java.lang.Deprecated annotation. This would allow me to use lein check to quickly list any calls to deprecated functions.

3 Answers

+1 vote
by

nowadays I think that it can be solved by a linter, like clj-kondo or lsp

by
I've just opened a problematic namespace in VSCode with Calva, which ships with both `clj-kondo` and `lsp`, and neither are reporting any problems with calls to deprecated functions.
+1 vote
by
edited by

Perhaps you can use a tool like jdeprscan on your emitted bytecode:

https://docs.oracle.com/javase/9/tools/jdeprscan.htm

$ mkdir -p src/foo
$ echo '(ns foo.core (:gen-class)) (Boolean. "true")' > src/foo/core.clj
$ mkdir -p classes
$ clojure -M -e "(compile 'foo.core)"
foo.core
$ $GRAALVM_HOME/bin/jdeprscan --class-path $(clojure -Spath) classes
Directory classes:
class foo/core__init uses deprecated method java/lang/Boolean::<init>(Ljava/lang/String;)V
0 votes
by

I've written a patch as a proof of concept:

 From 1c92777379bf74468a466dfe406825f5d5c8c4ba Mon Sep 17 00:00:00 2001
 From: Marc O'Morain <marc@circleci.com>
 Date: Wed, 29 Sep 2021 13:28:57 +0100
 Subject: [PATCH] Emit a warning when calling deprecated Java APIs
 
 Detect calls to decrecated Java methods, classes, and static methods.
 ---
  src/jvm/clojure/lang/Compiler.java            | 28 +++++++++++++++++++
  test/clojure/test_clojure/rt.clj              | 18 +++++++++++-
  .../ClassWithDeprecatedMethod.java            | 22 +++++++++++++++
  3 files changed, 67 insertions(+), 1 deletion(-)
  create mode 100644 test/java/compilation/ClassWithDeprecatedMethod.java
 
 diff --git a/src/jvm/clojure/lang/Compiler.java b/src/jvm/clojure/lang/Compiler.java
 index 041786e8..bb2172b6 100644
 --- a/src/jvm/clojure/lang/Compiler.java
 +++ b/src/jvm/clojure/lang/Compiler.java
 @@ -1151,16 +1151,19 @@ static class InstanceFieldExpr extends FieldExpr implements AssignableExpr{
  		this.target = target;
  		this.targetClass = target.hasJavaClass() ? target.getJavaClass() : null;
  		this.field = targetClass != null ? Reflector.getField(targetClass, fieldName, false) : null;
  		this.fieldName = fieldName;
  		this.line = line;
  		this.column = column;
  		this.tag = tag;
  		this.requireField = requireField;
 +
 +		// TODO: deprecation warning.
 +
  		if(field == null && RT.booleanCast(RT.WARN_ON_REFLECTION.deref()))
  			{
  			if(targetClass == null)
  				{
  				RT.errPrintWriter()
  					.format("Reflection warning, %s:%d:%d - reference to field %s can't be resolved.\n",
  									SOURCE_PATH.deref(), line, column, fieldName);
  				}
 @@ -1523,16 +1526,29 @@ static class InstanceMethodExpr extends MethodExpr{
  				method = m;
  				if(method == null && RT.booleanCast(RT.WARN_ON_REFLECTION.deref()))
  					{
  					RT.errPrintWriter()
  						.format("Reflection warning, %s:%d:%d - call to method %s on %s can't be resolved (argument types: %s).\n",
  							SOURCE_PATH.deref(), line, column, methodName, target.getJavaClass().getName(), getTypeStringForArgs(args));
  					}
  				}
 +
 +				if (isDeprecated(target.getJavaClass()) && RT.booleanCast(RT.WARN_ON_REFLECTION.deref()))
 +					{
 +					RT.errPrintWriter()
 +						.format("Deprecation warning, %s:%d:%d - class %s is deprecated.\n",
 +						SOURCE_PATH.deref(), line, column, target.getJavaClass().getSimpleName());
 +					}
 +				if (isDeprecated(method) && RT.booleanCast(RT.WARN_ON_REFLECTION.deref()))
 +					{
 +					RT.errPrintWriter()
 +						.format("Deprecation warning, %s:%d:%d - method %s is deprecated.\n",
 +						SOURCE_PATH.deref(), line, column, methodName);
 +					}
  			}
  		else
  			{
  			method = null;
  			if(RT.booleanCast(RT.WARN_ON_REFLECTION.deref()))
  				{
  				RT.errPrintWriter()
  					.format("Reflection warning, %s:%d:%d - call to method %s can't be resolved (target class is unknown).\n",
 @@ -1699,16 +1715,23 @@ static class StaticMethodExpr extends MethodExpr{
  					SOURCE_PATH.deref(), line, column, methodName, c.getName(), getTypeStringForArgs(args));
  			}
  		if(method != null && warnOnBoxedKeyword.equals(RT.UNCHECKED_MATH.deref()) && isBoxedMath(method))
  			{
  			RT.errPrintWriter()
  				.format("Boxed math warning, %s:%d:%d - call: %s.\n",
  						SOURCE_PATH.deref(), line, column, method.toString());
  			}
 +
 +		if (isDeprecated(method) && RT.booleanCast(RT.WARN_ON_REFLECTION.deref()))
 +			{
 +				RT.errPrintWriter()
 +					.format("Deprecation warning, %s:%d:%d - static method %s is deprecated.\n",
 +					SOURCE_PATH.deref(), line, column, methodName);
 +			}
  	}
  
  	public static boolean isBoxedMath(java.lang.reflect.Method m) {
  		Class c = m.getDeclaringClass();
  		if(c.equals(Numbers.class))
  			{
  			WarnBoxedMath boxedMath = m.getAnnotation(WarnBoxedMath.class);
  			if(boxedMath != null)
 @@ -9084,9 +9107,14 @@ static IPersistentCollection emptyVarCallSites(){return PersistentHashSet.EMPTY;
  //					RT.errPrintWriter()
  //							.format("stack map frame \"%s\" and \"%s\" on %s:%d:%d \n",
  //									type1, type2,
  //									SOURCE_PATH.deref(), LINE.deref(), COLUMN.deref());
  //				}
  		    }
  		};
      }
 +
 +static boolean isDeprecated(java.lang.reflect.AnnotatedElement ae) {
 +    return ae!= null && ae.getDeclaredAnnotation(Deprecated.class) != null;
 +}
 +
  }
 diff --git a/test/clojure/test_clojure/rt.clj b/test/clojure/test_clojure/rt.clj
 index 39526975..7c7bff3c 100644
 --- a/test/clojure/test_clojure/rt.clj
 +++ b/test/clojure/test_clojure/rt.clj
 @@ -61,17 +61,33 @@
       (defn foo [x] (.zap x 1))))
    (testing "reflection cannot resolve static method"
      (should-print-err-message
       #"Reflection warning, .*:\d+:\d+ - call to static method valueOf on java\.lang\.Integer can't be resolved \(argument types: java\.util\.regex\.Pattern\)\.\r?\n"
       (defn foo [] (Integer/valueOf #"boom"))))
    (testing "reflection cannot resolve constructor"
      (should-print-err-message
       #"Reflection warning, .*:\d+:\d+ - call to java\.lang\.String ctor can't be resolved\.\r?\n"
 -     (defn foo [] (String. 1 2 3)))))
 +     (defn foo [] (String. 1 2 3))))
 +  (testing "call to deprecated method"
 +    (should-print-err-message
 +     #"Deprecation warning, .*:\d+:\d+ - method increment is deprecated\.\r?\n"
 +     (defn foo []
 +       (.increment (compilation.ClassWithDeprecatedMethod.) 1))))
 +  (testing "call to method in deprecated class"
 +    (should-print-err-message
 +     #"Deprecation warning, .*:\d+:\d+ - class Inner is deprecated\.\r?\n"
 +     (defn foo []
 +       (.decrement (compilation.ClassWithDeprecatedMethod$Inner.) 1))))
 +  (testing "call to deprecated static method"
 +    (should-print-err-message
 +     #"Deprecation warning, .*:\d+:\d+ - static method empty is deprecate\.\r?\n"
 +     (defn foo []
 +       (compilation.ClassWithDeprecatedMethod/empty))))
 +  )
  
  (def example-var)
  (deftest binding-root-clears-macro-metadata
    (alter-meta! #'example-var assoc :macro true)
    (is (contains? (meta #'example-var) :macro))
    (.bindRoot #'example-var 0)
    (is (not (contains? (meta #'example-var) :macro))))
  
 diff --git a/test/java/compilation/ClassWithDeprecatedMethod.java b/test/java/compilation/ClassWithDeprecatedMethod.java
 new file mode 100644
 index 00000000..dab925aa
 --- /dev/null
 +++ b/test/java/compilation/ClassWithDeprecatedMethod.java
 @@ -0,0 +1,22 @@
 +package compilation;
 +
 +public class ClassWithDeprecatedMethod {
 +
 +    @Deprecated
 +    public int increment(int x) {
 +        return x + 1;
 +    }
 +
 +
 +    @Deprecated
 +    public static void empty() {
 +    }
 +
 +
 +    @Deprecated
 +    public static class Inner {
 +        public int decrement(int x) {
 +            return x - 1;
 +        }
 +    }
 +}
 -- 
 2.31.1
 
by
This patch works well, but it might be better if I printed the fully qualified class + method that is deprecated:

     marc@blaster ~/dev/congomongo $ lein check  2>&1 | grep deprecated
     OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
     Deprecation warning, somnium/congomongo.clj:149:21 - method getDB is deprecated.
     Deprecation warning, somnium/congomongo.clj:158:33 - method getDB is deprecated.
     Deprecation warning, somnium/congomongo.clj:319:5 - method getTime is deprecated.
     Deprecation warning, somnium/congomongo.clj:492:16 - method getCount is deprecated.
     Deprecation warning, somnium/congomongo.clj:508:22 - method hint is deprecated.
     Deprecation warning, somnium/congomongo.clj:511:20 - method setOptions is deprecated.
     Deprecation warning, somnium/congomongo.clj:729:52 - method outputMode is deprecated.
     Deprecation warning, somnium/congomongo.clj:755:15 - method getDB is deprecated.
     Deprecation warning, somnium/congomongo.clj:763:8 - method getDatabaseNames is deprecated.
...