Jak celowo wywołać niestandardowy komunikat ostrzegawczy kompilatora java?

84

Mam zamiar popełnić brzydki tymczasowy hack, aby obejść problem z blokowaniem, podczas gdy czekamy na naprawę zewnętrznego zasobu. Oprócz oznaczenia tego wielkim, przerażającym komentarzem i kilkoma FIXME, chciałbym, aby kompilator wyrzucił oczywiste ostrzeżenie jako przypomnienie, abyśmy nie zapomnieli o tym usunąć. Na przykład coś takiego:

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

Czy istnieje sposób, aby spowodować celowe ostrzeżenie kompilatora za pomocą wybranego przeze mnie komunikatu? Jeśli to się nie powiedzie, jaka jest najłatwiejsza rzecz do dodania do kodu, aby wyrzucić istniejące ostrzeżenie, być może z komunikatem w ciągu znaków w linii powodującej naruszenie, aby został wydrukowany w komunikacie ostrzegawczym?

EDYCJA: wycofane tagi wydają się nic nie robić:

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

Brak błędów kompilatora lub środowiska uruchomieniowego w eclipse lub z sun javac 1.6 (uruchamianym ze skryptu Ant) i zdecydowanie wykonuje tę funkcję.

pimlottc
źródło
1
FYI: @Deprecated daje tylko ostrzeżenie kompilatora , a nie błąd kompilatora lub czasu wykonania. Kod zdecydowanie powinien działać
BalusC
Spróbuj uruchomić bezpośrednio javac. Podejrzewam, że Ant ukrywa jakieś wyjście. Lub zobacz moją zaktualizowaną odpowiedź poniżej, aby uzyskać więcej informacji.
Peter Recore

Odpowiedzi:

42

Jedną techniką, którą widziałem, jest powiązanie tego z testami jednostkowymi ( robisz testy jednostkowe, prawda?). Zasadniczo tworzysz test jednostkowy, który kończy się niepowodzeniem po osiągnięciu poprawki do zasobów zewnętrznych. Następnie komentujesz ten test jednostkowy, aby powiedzieć innym, jak cofnąć swój gnarly hack, gdy problem zostanie rozwiązany.

To, co naprawdę sprytne w tym podejściu, polega na tym, że wyzwalaczem do cofnięcia włamania jest naprawa samego podstawowego problemu.

Kevin Day
źródło
2
Słyszałem o tym na jednej z konferencji No Fluff Just Stuff (nie pamiętam, kto był prezenterem). Myślałem, że to całkiem sprytne. Zdecydowanie polecam jednak te konferencje.
Kevin Day,
3
Chciałbym zobaczyć przykład takiego podejścia
birgersp
Odpowiedź ma 11 lat, ale poszedłbym nawet o krok dalej: komentowanie testów jednostkowych jest niebezpieczne. Stworzyłbym test jednostkowy, który zawierałby niepożądane zachowanie, więc kiedy ostatecznie zostanie naprawiony, komplikacje zepsują się.
avgvstvs
86

Myślę, że rozwiązaniem jest niestandardowa adnotacja, która zostanie przetworzona przez kompilator. Często piszę własne adnotacje, aby robić różne rzeczy w czasie wykonywania, ale nigdy nie próbowałem ich używać podczas kompilacji. Mogę więc podać tylko wskazówki dotyczące narzędzi, których możesz potrzebować:

  • Napisz niestandardowy typ adnotacji. Ta strona wyjaśnia, jak pisać adnotacje.
  • Napisz procesor adnotacji, który przetwarza twoją niestandardową adnotację, aby emitować ostrzeżenie. Narzędzie, które uruchamia takie procesory adnotacji, nazywa się APT. Na tej stronie można znaleźć wprowadzenie . Myślę, że to, czego potrzebujesz w APT API, to AnnotationProcessorEnvironment, który pozwoli ci emitować ostrzeżenia.
  • Począwszy od Java 6, APT jest zintegrowany z javac. Oznacza to, że można dodać procesor adnotacji w wierszu poleceń javac. W tej części podręcznika javac dowiesz się, jak zadzwonić do własnego procesora adnotacji.

Nie wiem, czy to rozwiązanie jest naprawdę wykonalne. Postaram się to zrealizować sam, gdy znajdę trochę czasu.

Edytować

Z powodzeniem wdrożyłem swoje rozwiązanie. Jako bonus, skorzystałem z narzędzia dostawcy usług Java, aby uprościć jego użycie. Właściwie moim rozwiązaniem jest słoik, który zawiera dwie klasy: niestandardową adnotację i procesor adnotacji. Aby go użyć, po prostu dodaj ten słoik do ścieżki klas swojego projektu i dodaj adnotacje, co chcesz! To działa dobrze w moim IDE (NetBeans).

Kod adnotacji:

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 {

}

Kod podmiotu przetwarzającego:

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

}

Aby udostępnić wynikowy plik jar jako usługodawcę, dodaj plik do pliku META-INF/services/javax.annotation.processing.Processorjar. Ten plik jest plikiem acsii, który musi zawierać następujący tekst:

fr.barjak.hack_processor.Processor
barjak
źródło
3
+1, świetne badania! Jest to zdecydowanie „właściwy sposób”, aby to zrobić (jeśli test jednostkowy nie jest praktyczny) i ma tę zaletę, że wyróżnia się ponad zwykłymi ostrzeżeniami.
Yishai
1
javac emituje ostrzeżenie, ale nic się nie dzieje podczas zaćmienia (?)
fwonce
8
Mała uwaga: nie ma potrzeby nadpisywania initi ustawiania envpola - możesz pobrać ProcessingEnvironmentz, this.processingEnvponieważ jest protected.
Paul Bellora
Czy ten komunikat ostrzegawczy będzie widoczny w ostrzeżeniach IDE?
uylmz
4
Przetwarzanie adnotacji jest domyślnie wyłączone w Eclipse. Aby go włączyć, przejdź do Właściwości projektu -> Kompilator Java -> Przetwarzanie adnotacji -> Włącz przetwarzanie adnotacji. Następnie poniżej tej strony znajduje się strona o nazwie „Ścieżka fabryczna”, na której należy skonfigurować pliki JAR z procesorami, których chcesz używać.
Konstantin Komissarchik
14

Szybkim i niezbyt brudnym podejściem może być użycie @SuppressWarningsadnotacji z celowo błędnym Stringargumentem:

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

Spowoduje to wygenerowanie ostrzeżenia, ponieważ kompilator nie rozpoznaje go jako specyficznego ostrzeżenia do pominięcia:

Nieobsługiwany @SuppressWarnings („FIXME: to jest hack i powinien zostać naprawiony”)

Luchostein
źródło
4
Nie działa w tłumieniu ostrzeżeń o widoczności pola lub błędów kłaczków.
IgorGanapolsky
2
Ironia jest rozpraszająca.
kambunctious
12

Jeden dobry hack zasługuje na inny ... Zwykle generuję ostrzeżenia kompilatora w opisanym celu, wprowadzając nieużywaną zmienną w metodzie hacky, a więc:

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

Ta nieużywana zmienna wygeneruje ostrzeżenie, które (w zależności od kompilatora) będzie wyglądać mniej więcej tak:

OSTRZEŻENIE: zmienna lokalna FIXMEtemporaryHackToWorkAroundLibraryBugRemoveMeWhenLibraryIsFixed nigdy nie jest odczytywana.

To rozwiązanie nie jest tak przyjemne jak niestandardowa adnotacja, ale ma tę zaletę, że nie wymaga wcześniejszego przygotowania (zakładając, że kompilator jest już skonfigurowany do wydawania ostrzeżeń dla nieużywanych zmiennych). Sugerowałbym, że takie podejście jest odpowiednie tylko w przypadku krótkotrwałych hacków. W przypadku długowiecznych hacków argumentowałbym, że wysiłek stworzenia niestandardowej adnotacji byłby uzasadniony.

WReach
źródło
Czy wiesz, jak włączyć ostrzeżenia o nieużywanych zmiennych? Tworzę dla Androida z Gradle z wiersza poleceń i nie otrzymuję żadnych ostrzeżeń o nieużywanych zmiennych. Czy wiesz, jak można to włączyć w build.gradle?
Andreas
@Andreas Przepraszamy, nie wiem nic o tym środowisku / łańcuchu narzędzi. Jeśli nie ma jeszcze pytania SO na ten temat, możesz rozważyć zadanie go.
WR Każdy
10

Napisałem bibliotekę, która robi to z adnotacjami: Lightweight Javac @Warning Annotation

Użycie jest bardzo proste:

// some code...

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

Kompilator wyśle ​​ostrzeżenie z twoim tekstem

Artem Zinnatullin
źródło
Dodaj zastrzeżenie, że jesteś autorem zalecanej biblioteki.
Paul Bellora
@PaulBellora nie wiem, jak ci to pomoże, ale w porządku
Artem Zinnatullin
2
Dzięki! Zobacz meta.stackexchange.com/questions/57497/…
Paul Bellora
5

co powiesz na oznaczenie metody lub klasy jako @Deprecated? dokumenty tutaj . Zauważ, że istnieje zarówno @Deprecated, jak i @deprecated - wersja z wielkiej litery D to adnotacja, a mała litera d to wersja javadoc. Wersja javadoc umożliwia określenie dowolnego ciągu wyjaśniającego, co się dzieje. Ale kompilatory nie są zobowiązane do wysyłania ostrzeżeń, gdy je widzą (chociaż wielu to robi). Adnotacja powinna zawsze powodować ostrzeżenie, chociaż nie sądzę, aby można było dodać do niej wyjaśnienie.

UPDATE tutaj jest kod, który właśnie przetestowałem z: Sample.java zawiera:

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

SampleCaller.java zawiera:

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

po uruchomieniu „javac Sample.java SampleCaller.java” otrzymuję następujące dane wyjściowe:

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

Używam Sun's javac 1.6. Jeśli chcesz mieć uczciwe ostrzeżenie, a nie tylko notatkę, użyj opcji -Xlint. Może to prawidłowo przesiąknie przez Ant.

Peter Recore
źródło
Wydaje się, że nie otrzymuję błędu od kompilatora z @Deprecate; edytuj mój q za pomocą przykładowego kodu.
pimlottc
1
hmm. Twój przykład pokazuje tylko przestarzałą metodę. gdzie używasz metody? tam pojawi się ostrzeżenie.
Peter Recore
3
Dla przypomnienia, @Deprecateddziała tylko między klasami (więc jest bezużyteczny w przypadku metod prywatnych).
npostavs
4

Możemy to zrobić za pomocą adnotacji!

Aby zgłosić błąd, użyj, Messageraby wysłać wiadomość z Diagnostic.Kind.ERROR. Krótki przykład:

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

Oto dość prosta adnotacja, którą napisałem, żeby to przetestować.

Ta @Markeradnotacja wskazuje, że celem jest interfejs znacznika:

package marker;

import java.lang.annotation.*;

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

A procesor adnotacji powoduje błąd, jeśli tak nie jest:

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

Na przykład są to prawidłowe zastosowania @Marker:

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

Ale te zastosowania @Markerspowodują błąd kompilatora:

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

    błąd znacznika

Oto post na blogu, który okazał się bardzo pomocny w rozpoczęciu tego tematu:


Mała uwaga: to, na co zwraca uwagę poniższy komentator, to fakt, że z powodu MarkerProcessorodniesień Marker.classma zależność od czasu kompilacji. Powyższy przykład napisałem z założeniem, że obie klasy będą znajdować się w tym samym pliku JAR (powiedzmy marker.jar), ale nie zawsze jest to możliwe.

Na przykład załóżmy, że istnieje plik JAR aplikacji z następującymi klasami:

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

Wówczas procesor dla @Annistnieje w osobnym JAR, który jest używany podczas kompilacji aplikacji JAR:

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

W takim przypadku AnnProcessornie byłoby możliwe bezpośrednie odwołanie się do typu @Ann, ponieważ spowodowałoby to utworzenie cyklicznej zależności JAR. Byłoby w stanie odwoływać się tylko @Annprzez Stringnazwę lub TypeElement/ TypeMirror.

Radiodef
źródło
To nie jest najlepszy sposób pisania procesorów adnotacji. Zwykle uzyskujesz typ adnotacji z Set<? extends TypeElement>parametru, a następnie uzyskujesz elementy z adnotacjami dla danej rundy za pomocą getElementsAnnotatedWith(TypeElement annotation). Nie rozumiem też, dlaczego zapakowałeś printMessagemetodę.
ThePyroEagle
@ThePyroEagle Wybór między dwoma przeciążeniami jest z pewnością niewielką różnicą w stylu kodowania.
Radiodef
Jest, ale idealnie, czy nie chciałbyś mieć po prostu procesora adnotacji w pliku JAR procesora? Korzystanie z wyżej wymienionych metod pozwala na taki poziom izolacji, ponieważ nie trzeba mieć przetworzonej adnotacji w ścieżce klas.
ThePyroEagle
2

Tutaj pokazuje samouczek dotyczący adnotacji, a na dole daje przykład definiowania własnych adnotacji. Niestety szybkie przejrzenie samouczka powiedziało, że są one dostępne tylko w javadoc ...

Adnotacje używane przez kompilator Istnieją trzy typy adnotacji, które są wstępnie zdefiniowane przez samą specyfikację języka: @Deprecated, @Override i @SuppressWarnings.

Wygląda więc na to, że wszystko, co naprawdę możesz zrobić, to wrzucić tag @Deprecated, który kompilator wydrukuje lub umieści w javadocs niestandardowy tag, który mówi o włamaniu.

Matt Phillips
źródło
Ponadto kompilator wyemituje ostrzeżenie, mówiąc, że metoda oznaczona @Deprecated jest taka ... Poinformuje użytkownika, która z nich jest obraźliwa.
Matt Phillips
1

Jeśli używasz IntelliJ. Możesz przejść do: Preferencje> Edytor> TODO i dodać „\ bhack.b *” lub inny wzorzec.

Jeśli następnie skomentujesz // HACK: temporary fix to work around server issues

Następnie w oknie narzędzia TODO będzie się ładnie wyświetlać wraz ze wszystkimi innymi zdefiniowanymi wzorcami podczas edycji.

BARJ
źródło
0

Do kompilacji należy użyć narzędzia, takiego jak ant ou maven. Dzięki niemu powinieneś zdefiniować pewne zadania w czasie kompilacji, które mogą na przykład generować dzienniki (takie jak komunikaty lub ostrzeżenia) dotyczące tagów FIXME.

A jeśli chcesz błędów, to też jest możliwe. Na przykład zatrzymaj kompilację, gdy zostawisz w kodzie trochę TODO (dlaczego nie?)

Lioda
źródło
Hack polega na tym, żeby działał jak najszybciej, nie mam czasu na zmianę systemu kompilacji w tej chwili :) Ale dobrze jest myśleć o przyszłości ...
pimlottc
0

Aby w ogóle pojawiło się jakiekolwiek ostrzeżenie, stwierdziłem, że nieużywane zmienne i niestandardowe @SuppressWarnings nie działają dla mnie, ale niepotrzebne rzutowanie tak:

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

Teraz, kiedy kompiluję:

$ javac -Xlint:all Example.java
ExampleTest.java:12: warning: [cast] redundant cast to String
        String s = (String) "Hello!";
                   ^
1 warning
Andy Balaam
źródło