How to intentionally cause a custom java compiler warning message?
Asked Answered
M

11

97

I'm about to commit an ugly temporary hack in order to work around a blocking issue while we wait for an external resource to be fixed. Aside from marking it with a big scary comment and a bunch of FIXMEs, I'd love to have the compiler throw an obvious warning message as a reminder so we don't forget to take this out. For example, something like:

[javac] com.foo.Hacky.java:192: warning: FIXME temporary hack to work around library bug, remove me when library is fixed!

Is there a way I can cause an intentional compiler warning with a message of my choosing? Failing that, what's the easiest thing to add to the code to throw an existing warning, with perhaps a message in a string on the offending line so it gets printed in the warning message?

EDIT: Deprecated tags don't seem to be doing anything for me:

/**
 * @deprecated "Temporary hack to work around remote server quirks"
 */
@Deprecated
private void doSomeHackyStuff() { ... }

No compiler or runtime errors in eclipse or from sun javac 1.6 (running from ant script), and it's definitely executing the function.

Miracle answered 17/11, 2009 at 23:27 Comment(2)
FYI: the @Deprecated only gives a compiler warning, not a compiler or runtime error. The code should definitely runDispenser
Try running with javac directly. I suspect Ant is hiding some output. Or see my updated answer below for more details.Velodrome
C
45

One technique that I've seen used is to tie this into unit testing (you do unit test, right?). Basically you create a unit test that fails once the external resource fix is achieved. Then you comment that unit test to tell others how to undo your gnarly hack once the issue is resolved.

What's really slick about this approach is that the trigger for undoing your hack is a fix of the core issue itself.

Cursory answered 18/11, 2009 at 2:44 Comment(3)
I heard about this at one of the No Fluff Just Stuff conferences (can't remember who the presenter was). I thought it was pretty slick. I definitely recommend those conferences, though.Cursory
I'd like to see an example of this approachInitiatory
Answer is 11yrs old, but I'd even take that a step further: commenting unit tests is dangerous. I'd create a unit test that encapsulates the undesired behavior, so that way when it eventually gets fixed, compliation breaks.Barbusse
C
95

I think that a custom annotation, which will be processed by the compiler, is the solution. I frequently write custom annotations to do things at runtime, but I never tried to use them at compilation time. So, I can only give you pointers on the tools you may need :

  • Write a custom annotation type. This page explains how to write an annotation.
  • Write an annotation processor, that processes your custom annotation to emit a warning. The tool that runs such annotation processors is called APT. You can find an indroduction on this page. I think what you need in the APT API is AnnotationProcessorEnvironment, which will let you emit warnings.
  • From Java 6, APT is integrated into javac. That is, you can add an annotation processor in the javac command line. This section of the javac manual will tell you how to call your custom annotation processor.

I don't know if this solution is really practicable. I'll try to implement it myself when I find some time.

Edit

I successfully implemented my solution. And as a bonus, I used java's service provider facility to simplify its use. Actually, my solution is a jar that contains 2 classes : the custom annotation and the annotation processor. To use it, just add this jar in the classpath of your project, and annotate whatever you want ! This is working fine right inside my IDE (NetBeans).

Code of the annotation :

package fr.barjak.hack;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElementType.METHOD, ElementType.PACKAGE, ElementType.PARAMETER, ElementType.TYPE})
public @interface Hack {

}

Code of the processor :

package fr.barjak.hack_processor;

import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;

@SupportedAnnotationTypes("fr.barjak.hack.Hack")
public class Processor extends AbstractProcessor {

    private ProcessingEnvironment env;

    @Override
    public synchronized void init(ProcessingEnvironment pe) {
        this.env = pe;
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (!roundEnv.processingOver()) {
            for (TypeElement te : annotations) {
                final Set< ? extends Element> elts = roundEnv.getElementsAnnotatedWith(te);
                for (Element elt : elts) {
                    env.getMessager().printMessage(Kind.WARNING,
                            String.format("%s : thou shalt not hack %s", roundEnv.getRootElements(), elt),
                            elt);
                }
            }
        }
        return true;
    }

}

To enable the resulting jar as a service provider, add the file META-INF/services/javax.annotation.processing.Processor in the jar. This file is an acsii file that must contain the following text :

fr.barjak.hack_processor.Processor
Creatine answered 18/11, 2009 at 9:48 Comment(7)
+1, Great research! This is definitely the "right way" to do it (if a unit test is not practical), and it has the advantage of standing out over and above the regular warnings.Ani
javac emits a warning, but nothing happens in eclipse(?)Disjunction
Small note: there's no need to override init and set the env field - you can get the ProcessingEnvironment from this.processingEnv since it's protected.Selfconfessed
Will this warning message be visible on IDE warnings?Irvine
These are real javac warning, so if your IDE actually calls javac and parses its output to find the warnings, then they will be visible in your IDE. If your IDE tries to be smarter and uses an internal compiler, and if this compiler doesn't care about the service provider we declared, then they won't be visible.Creatine
Annotation processing is off by default in Eclipse. To turn it on, go to Project Properties -> Java Compiler -> Annotation Processing -> Enable annotation processing. Then beneath that page is a page called "Factory Path" where you will need to configure jars that have the processors you want to use.Greaten
I am trying to apply this solution to Android but can't make it work. Basically I have compiled the two files above with "javac *.java". Then I put them under the folders "fr/barjak/hack" and "fr/barjak/hack_processor". Then I run "jar cf hack.jar fr" to generate the jar file. Then I import that jar into IntelliJ and use the annotation @Hack but when I build the project nothing gets displayed. Any ideas??Govea
C
45

One technique that I've seen used is to tie this into unit testing (you do unit test, right?). Basically you create a unit test that fails once the external resource fix is achieved. Then you comment that unit test to tell others how to undo your gnarly hack once the issue is resolved.

What's really slick about this approach is that the trigger for undoing your hack is a fix of the core issue itself.

Cursory answered 18/11, 2009 at 2:44 Comment(3)
I heard about this at one of the No Fluff Just Stuff conferences (can't remember who the presenter was). I thought it was pretty slick. I definitely recommend those conferences, though.Cursory
I'd like to see an example of this approachInitiatory
Answer is 11yrs old, but I'd even take that a step further: commenting unit tests is dangerous. I'd create a unit test that encapsulates the undesired behavior, so that way when it eventually gets fixed, compliation breaks.Barbusse
C
17

Some quick and not so dirty approach, may be to use a @SuppressWarnings annotation with a deliberately wrong String argument:

@SuppressWarnings("FIXME: this is a hack and should be fixed.")

This will generate a warning because it is not recognized by the compiler as a specific warning to suppress:

Unsupported @SuppressWarnings("FIXME: this is a hack and should be fixed.")

Crelin answered 20/2, 2015 at 21:16 Comment(2)
It doesn't work in suppressing field visibility warnings or lint errors.Anacreon
The irony is distracting.Forefinger
L
14

One good hack deserves another... I usually generate compiler warnings for the described purpose by introducing an unused variable in the hacky method, thus:

/**
 * @deprecated "Temporary hack to work around remote server quirks"
 */
@Deprecated
private void doSomeHackyStuff() {
    int FIXMEtemporaryHackToWorkAroundLibraryBugRemoveMeWhenLibraryIsFixed;
    ...
}

This unused variable will generate a warning which (depending upon your compiler) will look something like this:

WARNING: The local variable FIXMEtemporaryHackToWorkAroundLibraryBugRemoveMeWhenLibraryIsFixed is never read.

This solution is not as nice as a custom annotation, but it has the advantage that it requires no advance preparation (assuming the compiler is already configured to issue warnings for unused variables). I would suggest that this approach is only suitable for short-lived hacks. For long-lived hacks, I would argue that effort to create a custom annotation would be justified.

Leonoreleonsis answered 21/7, 2011 at 17:43 Comment(2)
Do you know how to enable unused variable warnings? I'm building for Android with Gradle from the command line and I don't get any warnings for unused variables. Do you know how this can be enabled in build.gradle?Fidgety
@Fidgety Sorry, I do not know anything about that environment/toolchain. If there is not already a SO question on this subject, you might consider asking one.Leonoreleonsis
A
13

I wrote a library that does this with annotations: Lightweight Javac @Warning Annotation

Usage is very simple:

// some code...

@Warning("This method should be refactored")
public void someCodeWhichYouNeedAtTheMomentButYouWantToRefactorItLater() {
    // bad stuff going on here...
}

And compiler will throw warning message with your text

Ayurveda answered 10/1, 2015 at 0:2 Comment(3)
Please add a disclaimer that you are the author of the recommended library.Selfconfessed
@PaulBellora don't know how it'll help you, but okayAyurveda
Thanks! See meta.stackexchange.com/questions/57497/…Selfconfessed
V
5

how about marking the method or class as @Deprecated? docs here. Note that there is both a @Deprecated and a @deprecated - the capital D version is the annotation and the lowercase d is the javadoc version. The javadoc version allows you to specify an arbitrary string explaining what is going on. But compilers are not required to emit a warning when seeing it (though many do). The annotation should always cause a warning, though i don't think you can add an explanation to it.

UPDATE here is the code I just tested with: Sample.java contains:

public class Sample {
    @Deprecated
    public static void foo() {
         System.out.println("I am a hack");
    }
}

SampleCaller.java contains:

public class SampleCaller{
     public static void main(String [] args) {
         Sample.foo();
     }
}

when i run "javac Sample.java SampleCaller.java" i get the following output:

Note: SampleCaller.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

I am using sun's javac 1.6. If you want an honest to goodness warning rather than just a note, use the -Xlint option. Maybe that will percolate up through Ant properly.

Velodrome answered 17/11, 2009 at 23:30 Comment(3)
I don't seem to get an error from the compiler with @Deprecate; edit my q with example code.Miracle
hmm. your example only shows the deprecated method. where do you use the method? that's where the warning will show up.Velodrome
For the record, @Deprecated only works across classes (so it's useless for private methods).Inter
A
5

We can do this with annotations!

To raise an error, use Messager to send a message with Diagnostic.Kind.ERROR. Short example:

processingEnv.getMessager().printMessage(
    Diagnostic.Kind.ERROR, "Something happened!", element);

Here's a fairly simple annotation I wrote just to test this out.

This @Marker annotation indicates the target is a marker interface:

package marker;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Marker {
}

And the annotation processor causes an error if it's not:

package marker;

import javax.annotation.processing.*;
import javax.lang.model.*;
import javax.lang.model.element.*;
import javax.lang.model.type.*;
import javax.lang.model.util.*;
import javax.tools.Diagnostic;
import java.util.Set;

@SupportedAnnotationTypes("marker.Marker")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public final class MarkerProcessor extends AbstractProcessor {

    private void causeError(String message, Element e) {
        processingEnv.getMessager()
            .printMessage(Diagnostic.Kind.ERROR, message, e);
    }

    private void causeError(
            Element subtype, Element supertype, Element method) {
        String message;
        if (subtype == supertype) {
            message = String.format(
                "@Marker target %s declares a method %s",
                subtype, method);
        } else {
            message = String.format(
                "@Marker target %s has a superinterface " +
                "%s which declares a method %s",
                subtype, supertype, method);
        }

        causeError(message, subtype);
    }

    @Override
    public boolean process(
            Set<? extends TypeElement> annotations,
            RoundEnvironment roundEnv) {

        Elements elementUtils = processingEnv.getElementUtils();
        boolean processMarker = annotations.contains(
            elementUtils.getTypeElement(Marker.class.getName()));
        if (!processMarker)
            return false;

        for (Element e : roundEnv.getElementsAnnotatedWith(Marker.class)) {
            ElementKind kind = e.getKind();

            if (kind != ElementKind.INTERFACE) {
                causeError(String.format(
                    "target of @Marker %s is not an interface", e), e);
                continue;
            }

            if (kind == ElementKind.ANNOTATION_TYPE) {
                causeError(String.format(
                    "target of @Marker %s is an annotation", e), e);
                continue;
            }

            ensureNoMethodsDeclared(e, e);
        }

        return true;
    }

    private void ensureNoMethodsDeclared(
            Element subtype, Element supertype) {
        TypeElement type = (TypeElement) supertype;

        for (Element member : type.getEnclosedElements()) {
            if (member.getKind() != ElementKind.METHOD)
                continue;
            if (member.getModifiers().contains(Modifier.STATIC))
                continue;
            causeError(subtype, supertype, member);
        }

        Types typeUtils = processingEnv.getTypeUtils();
        for (TypeMirror face : type.getInterfaces()) {
            ensureNoMethodsDeclared(subtype, typeUtils.asElement(face));
        }
    }
}

For example, these are correct uses of @Marker:

  • @Marker
    interface Example {}
    
  • @Marker
    interface Example extends Serializable {}
    

But these uses of @Marker will cause a compiler error:

  • @Marker
    class Example {}
    
  • @Marker
    interface Example {
        void method();
    }
    

    marker error

Here's a blog post I found very helpful getting started on the subject:


Small note: what the commentor below is pointing out is that because MarkerProcessor references Marker.class, it has a compile-time dependency on it. I wrote the above example with the assumption that both classes would go in the same JAR file (say, marker.jar), but that's not always possible.

For example, suppose there's an application JAR with the following classes:

com.acme.app.Main
com.acme.app.@Ann
com.acme.app.AnnotatedTypeA (uses @Ann)
com.acme.app.AnnotatedTypeB (uses @Ann)

Then the processor for @Ann exists in a separate JAR, which is used while compiling the application JAR:

com.acme.proc.AnnProcessor (processes @Ann)

In that case, AnnProcessor would not be able to reference the type of @Ann directly, because it would create a circular JAR dependency. It would only be able to reference @Ann by String name or TypeElement/TypeMirror.

Armistice answered 26/4, 2015 at 18:12 Comment(3)
That's not exactly the best way of writing annotation processors. You usually get the annotation type from the Set<? extends TypeElement> parameter and then get the annotated elements for the given round using getElementsAnnotatedWith(TypeElement annotation). I also don't understand why you wrapped the printMessage method.Brandtr
@Brandtr The choice between two overloads sure is a pretty small difference in coding style.Armistice
It is but ideally, wouldn't you want to just have the annotation processor in the processor JAR? Using the previously mentioned methods allows for that level of isolation as you don't need to have the processed annotation in the classpath.Brandtr
L
2

Here shows a tutorial on annotations and at the bottom it gives an example of defining your own annotations. Unfortunately a quick skimming of the tutorial said that those are only available in the javadoc...

Annotations Used by the Compiler There are three annotation types that are predefined by the language specification itself: @Deprecated, @Override, and @SuppressWarnings.

So it appears that all you can really do is throw in an @Deprecated tag that the compiler will print out or put a custom tag in the javadocs that tells about the hack.

Lesson answered 17/11, 2009 at 23:57 Comment(1)
also the compiler will emit a warning saying the method you mark with @Deprecated is so...It will tell the user which offending one it is.Lesson
C
1

If you're using IntelliJ. You can go to: Preferences>Editor>TODO and add "\bhack.b*" or any other pattern.

If you then make a comment like // HACK: temporary fix to work around server issues

Then in the TODO tool window, it will show up nicely, together with all your other defined patterns, while editing.

Clamor answered 27/1, 2017 at 9:8 Comment(0)
D
0

You should use a tool to compile, like ant ou maven. With it, you should define some tasks at compile time which could produce some logs (like messages or warnings) about your FIXME tags, for example.

And if you want some errors, it is possible too. Like stop compilation when you have left some TODO in your code (why not ?)

Dipterocarpaceous answered 17/11, 2009 at 23:46 Comment(1)
The hack is to get it working ASAP, I don't exactly have time to change the build system right now :) But good to think about for the future...Miracle
B
0

To get any warning at all to appear, I found that unused variables and custom @SuppressWarnings didn't work for me, but an unnecessary cast did:

public class Example {
    public void warn() {
        String fixmePlease = (String)"Hello";
    }
}

Now when I compile:

$ javac -Xlint:all Example.java
ExampleTest.java:12: warning: [cast] redundant cast to String
        String s = (String) "Hello!";
                   ^
1 warning
Briolette answered 23/12, 2016 at 14:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.