web-dev-qa-db-fra.com

Comment provoquer intentionnellement un message d'avertissement du compilateur Java personnalisé?

Je suis sur le point de commettre un piratage temporaire moche afin de contourner un problème de blocage en attendant la résolution d'une ressource externe. En plus de le marquer avec un gros commentaire effrayant et un groupe de FIXME, j'aimerais que le compilateur envoie un message d'avertissement évident comme rappel afin que nous n'oubliions pas de le supprimer. Par exemple, quelque chose comme:

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

Est-il possible de provoquer un avertissement intentionnel du compilateur avec un message de mon choix? En cas d'échec, quelle est la chose la plus facile à ajouter au code pour lancer un avertissement existant, avec éventuellement un message dans une chaîne sur la ligne incriminée afin qu'il soit imprimé dans le message d'avertissement?

EDIT: Les tags obsolètes ne semblent rien faire pour moi:

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

Aucune erreur de compilation ou d'exécution dans Eclipse ou Sun Javac 1.6 (en cours d'exécution depuis un script ant), et la fonction est définitivement exécutée.

71
pimlottc

Une technique que j'ai déjà utilisée consiste à intégrer cela au test unitaire (vous faites test unitaire, non?). En gros, vous créez un test unitaire qui échoue une fois le correctif des ressources externes atteint. Ensuite, vous commentez ce test unitaire pour dire aux autres comment effacer votre piratage informatique une fois le problème résolu.

Ce qui est vraiment génial avec cette approche est que le déclencheur pour annuler votre piratage informatique est une solution au problème principal lui-même.

32
Kevin Day

Je pense qu'une annotation personnalisée, qui sera traitée par le compilateur, est la solution. J'écris souvent des annotations personnalisées pour faire des choses au moment de l'exécution, mais je n'ai jamais essayé de les utiliser au moment de la compilation. Je ne peux donc que vous donner des indications sur les outils dont vous pourriez avoir besoin:

  • Écrivez un type d'annotation personnalisé. Cette page explique comment écrire une annotation.
  • Ecrivez un processeur d’annotation qui traite votre annotation personnalisée pour émettre un avertissement. L'outil qui exécute de tels processeurs d'annotation s'appelle APT. Vous pouvez trouver une introduction sur cette page . Je pense que ce dont vous avez besoin dans l'API APT est AnnotationProcessorEnvironment, qui vous permettra d'émettre des avertissements.
  • À partir de Java 6, APT est intégré à javac. En d’autres termes, vous pouvez ajouter un processeur d’annotation dans la ligne de commande javac. Cette section du manuel javac vous expliquera comment appeler votre processeur d'annotation personnalisé.

Je ne sais pas si cette solution est vraiment réalisable. Je vais essayer de le mettre en œuvre moi-même quand je trouverai du temps.

Modifier

J'ai implémenté avec succès ma solution. Et en prime, j'ai utilisé le service fournisseur de services de Java pour en simplifier l'utilisation. En fait, ma solution est un fichier jar contenant 2 classes: l'annotation personnalisée et le processeur d'annotation. Pour l'utiliser, ajoutez simplement ce fichier dans le chemin de classe de votre projet et annotez ce que vous voulez! Cela fonctionne très bien dans mon IDE (NetBeans).

Code de l'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 du processeur:

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;
    }

}

Pour activer le fichier JAR résultant en tant que fournisseur de services, ajoutez le fichier META-INF/services/javax.annotation.processing.Processor dans le fichier JAR. Ce fichier est un fichier acsii qui doit contenir le texte suivant:

fr.barjak.hack_processor.Processor
78
barjak

Une approche rapide et pas si sale peut consister à utiliser une annotation @SuppressWarnings avec un argument délibérément incorrect String:

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

Cela générera un avertissement car le compilateur ne le reconnaît pas comme un avertissement spécifique à supprimer:

@SuppressWarnings non pris en charge ("FIXME: ceci est un hack et devrait être Corrigé.")

12
Luchostein

Un bon hack en mérite un autre ... Je génère généralement des avertissements du compilateur dans le but décrit en introduisant une variable inutilisée dans la méthode hacky, ainsi:

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

Cette solution n’est pas aussi agréable qu’une annotation personnalisée, mais elle présente l’avantage de ne nécessiter aucune préparation préalable (en supposant que le compilateur soit déjà configuré pour émettre des avertissements pour les variables non utilisées). Je suggérerais que cette approche ne convient que pour les piratages de courte durée. Pour les hackers de longue durée, je dirais que l’effort de créer une annotation personnalisée serait justifié.

12
WReach

J'ai écrit une bibliothèque qui le fait avec des annotations: Lightweight Javac @Warning Annotation

L'utilisation est très simple: 

// some code...

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

Et le compilateur lancera un message d'avertissement avec votre texte

8
Artem Zinnatullin

que diriez-vous de marquer la méthode ou la classe comme @Deprecated? docs ici . Notez qu'il existe à la fois une @Deprecated et une @deprecated - la version D principale est l'annotation et la minuscule d est la version javadoc. La version javadoc vous permet de spécifier une chaîne arbitraire expliquant ce qui se passe. Mais les compilateurs ne sont pas obligés d'émettre un avertissement lorsqu'ils le voient (bien que beaucoup le fassent). L'annotation devrait toujours provoquer un avertissement, même si je ne pense pas que vous puissiez y ajouter une explication. 

UPDATE voici le code que je viens de tester avec: Sample.Java contient:

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

SampleCaller.Java contient:

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

quand je lance "javac Sample.Java SampleCaller.Java", j'obtiens le résultat suivant:

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

J'utilise le javac 1.6 de Sun. Si vous souhaitez un avertissement honnête, pas une simple note, utilisez l’option -Xlint. Peut-être que cela se répercutera correctement à travers Ant.

5
Peter Recore

Nous pouvons le faire avec des annotations!

Pour générer une erreur, utilisez Messager pour envoyer un message avec Diagnostic.Kind.ERROR . Petit exemple:

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

Voici une annotation assez simple que j'ai écrite juste pour tester cela.

Cette annotation @Marker indique que la cible est une interface de marqueur:

package marker;

import Java.lang.annotation.*;

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

Et le processeur d'annotation provoque une erreur si ce n'est pas le cas:

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));
        }
    }
}

Par exemple, ce sont des utilisations correctes de @Marker:

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

Mais ces utilisations de @Marker provoqueront une erreur de compilation:

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

    marker error

Voici un article de blog que j'ai trouvé très utile pour commencer sur le sujet:


Petite remarque: le commentaire ci-dessous indique que, étant donné que MarkerProcessor fait référence à Marker.class, il a une dépendance à la compilation. J'ai écrit l'exemple ci-dessus en supposant que les deux classes iraient dans le même fichier JAR (par exemple, marker.jar), mais ce n'est pas toujours possible.

Par exemple, supposons qu’il existe un JAR d’application avec les classes suivantes:

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

Ensuite, le processeur pour @Ann existe dans un fichier JAR séparé, utilisé lors de la compilation du fichier JAR de l'application:

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

Dans ce cas, AnnProcessor ne pourrait pas faire directement référence au type @Ann, car cela créerait une dépendance JAR circulaire. Il ne pourrait référencer que @Ann par String name ou TypeElement/TypeMirror.

3
Radiodef

Ici montre un tutoriel sur les annotations et en bas, un exemple de définition de vos propres annotations. Malheureusement, un rapide survol du tutoriel indique que ceux-ci ne sont disponibles que dans le javadoc ... 

Annotations utilisées par le compilateur Il existe trois types d'annotation qui sont prédéfinis par la spécification de langue elle-même: @Deprecated, @Override et @SuppressWarnings.

Il semble donc que tout ce que vous pouvez réellement faire est d’ajouter une balise @Deprecated que le compilateur imprimera ou de placer une balise personnalisée dans les javadocs qui indique le hack.

2
Matt Phillips

Vous devriez utiliser un outil de compilation, comme ant ou maven. Lors de la compilation, vous devez définir certaines tâches pouvant générer des journaux (tels que des messages ou des avertissements) concernant vos balises FIXME, par exemple.

Et si vous voulez des erreurs, c'est aussi possible. Comme arrêter la compilation quand vous avez laissé du TODO dans votre code (pourquoi pas?)

0
Lioda

Pour faire apparaître un quelconque avertissement, j’ai constaté que les variables inutilisées et les @SuppressWarnings personnalisés ne fonctionnaient pas pour moi, mais un casting inutile ne fonctionnait pas:

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

Maintenant, quand je compile:

$ javac -Xlint:all Example.Java
ExampleTest.Java:12: warning: [cast] redundant cast to String
        String s = (String) "Hello!";
                   ^
1 warning
0
Andy Balaam

Si vous utilisez IntelliJ. Vous pouvez aller à: Préférences> Editeur> TODO et ajouter "\ bhack.b *" ou n’importe quel autre motif. 

Si vous faites ensuite un commentaire comme // HACK: temporary fix to work around server issues

Ensuite, dans la fenêtre de l’outil TODO, il s’affichera bien, ainsi que tous vos autres modèles définis, lors de l’édition.

0
BARJ