Rozwiązywanie ostrzeżeń analizy statycznej dla warunków zawsze fałszywych

Jak rozwiązywać ostrzeżenia analizy statycznej dotyczące warunków zawsze fałszywych i nieosiągalnych gałęzi

Twój analizator statyczny oznaczył warunek jako zawsze fałszywy. Ostrzeżenie wygląda następująco, w zależności od narzędzia:

  • SonarQube: „Wyrażenia warunkowe, które zawsze są prawdą lub fałszem, mogą prowadzić do nieosiągalnego kodu” (RSPEC-2583)
  • IntelliJ / Inspectopedia: „Kod nie jest analizowany, ponieważ warunek jest statycznie oceniany jako fałsz”
  • Checkmarx / Fortify: „Nieosiągalny kod, wykryto martwą gałąź”
  • ESLint: „brak stałego warunku, nieoczekiwany stały warunek”
  • Pylint: „W0125: Użycie instrukcji warunkowej ze stałą wartością”

Wszystkie te stwierdzenia mówią to samo: analizator udowodnił w trakcie analizy, bez uruchamiania kodu, że konkretna gałąź instrukcji warunkowej nigdy nie może zostać wykonana. Warunek, który ją chroni, jest zawsze fałszywy. Wszystko w tej gałęzi to martwy kod.

W tym przewodniku opisano przyczyny tych ostrzeżeń, sposób identyfikowania kategorii, do której należy każde ostrzeżenie, prawidłowe rozwiązanie dla każdej kategorii, sytuacje, w których pominięcie ostrzeżenia jest właściwe, a także to, co zawsze fałszywe warunki w starszym kodzie COBOL ujawniają na temat zakresu migracji.

Wycofane reguły biznesowe nadal znajdują się w Twoim kodzie

SMART TS XL identyfikuje gałęzie EVALUATE odwołujące się do kodów produktów, wartości statusu i typów klientów, które nie istnieją już w danych produkcyjnych.

DOWIEDZ SIĘ WIĘCEJ…

Jak analiza statyczna wykrywa warunki zawsze fałszywe

Nowoczesne analizatory statyczne wykorzystują symboliczne wykonywanie i analizę przepływu danych, a nie proste dopasowywanie wzorców, do wykrywania warunków zawsze fałszywych. Analizator śledzi możliwe wartości każdej zmiennej w każdym punkcie programu. Po osiągnięciu warunku ocenia, czy warunek ten może być prawdziwy, biorąc pod uwagę znane ograniczenia zmiennych.

Istnieją trzy podstawowe mechanizmy, które zawsze skutkują fałszywymi wykryciami:

Symboliczna egzekucja. Analizator przypisuje zmiennym wartości symboliczne i śledzi, w jaki sposób operacje ograniczają te wartości. Jeśli zmienna x jest przypisany 0 w linii 3 i porównano x > 10 w wierszu 7 bez żadnego pośredniego przypisania warunek jest udowodnionym fałszem.

Ograniczenia systemu typów. Niektóre warunki są niemożliwe do spełnienia ze względu na typ zmiennej. Porównanie odwołania do obiektu innego niż null z null po asercji innej niż null lub porównanie wartości wyliczeniowej z przypadkiem spoza zdefiniowanego zbioru.

Uproszczenie algebry Boole'a. Warunki złożone, które upraszczają się do false niezależnie od wartości zmiennych: (a && !a), (x > 10 && x < 5), (!flag || false).

Reguła RSPEC-2583 w SonarQube wykorzystuje wykonywanie symboliczne: śledzi stan zmiennych poprzez graf przepływu sterowania i sygnalizuje warunki, w których stan symboliczny sprawia, że ​​warunek jest udowodniony jako fałszywy. Ostrzeżenie IntelliJ „warunek statycznie oceniony jako fałszywy” wykorzystuje to samo podejście.

Cztery główne przyczyny

Prawidłowe rozwiązanie problemu zawsze fałszywego ostrzeżenia zależy od prawidłowego zidentyfikowania, która z czterech głównych przyczyn ma zastosowanie. Rozwiązanie jest różne dla każdej z nich.

Przyczyna główna 1: Sprzeczność logiczna

Warunek zawiera logiczną niemożliwość, dwa podwarunki, które nie mogą być jednocześnie prawdziwe lub złożony warunek, który sprowadza się do false przez algebrę Boole’a.

Jawa

// Example: mutually exclusive subconditions
int score = getScore();
if (score > 90 && score < 50) {  // Always false: no integer is both > 90 and < 50
    applyBonus();                 // Never executes
}

// Example: boolean tautology negation
boolean active = isActive();
if (active && !active) {  // Always false: a && !a = false
    sendNotification();   // Never executes
}

// Example: range check already guaranteed
if (value >= 0) {
    if (value < 0) {  // Always false: already know value >= 0
        handleNegative();  // Never executes
    }
}

Rozkład: Napraw logikę. Albo warunek zawiera literówkę (programista miał na myśli score > 90 || score < 50) lub gałąź powinna zostać usunięta. W przykładzie sprawdzania zakresu gałąź wewnętrzna if jest zbędny i powinien zostać usunięty.

Przyczyna główna 2: Skumulowane zmiany reguł biznesowych

To najczęstsza przyczyna w długowiecznych bazach kodu. Warunek, który był prawidłowy i sensowny w momencie jego powstania, stał się trwale fałszywy z powodu późniejszych zmian w otaczającym go kodzie, bez usunięcia martwej gałęzi.

Jawa

// 2018: processType could be A, B, or C
// 2023: type C was deprecated and removed from the system
// The processType field is now guaranteed to never equal "C"
// but nobody removed the branch that handled it

public void process(String processType) {
    if (processType.equals("A")) {
        handleTypeA();
    } else if (processType.equals("B")) {
        handleTypeB();
    } else if (processType.equals("C")) {  // Always false since 2023
        handleTypeC();                      // 47 lines of dead business logic
    }
}

pyton

# Python equivalent: accumulated rule changes
def calculate_rate(customer_tier: str) -> float:
    if customer_tier == "GOLD":
        return 0.05
    elif customer_tier == "SILVER":
        return 0.03
    elif customer_tier == "BRONZE":
        # BRONZE tier was eliminated in Q2 2021
        # All BRONZE customers were migrated to SILVER
        # This branch has been dead for 4+ years
        return 0.01  # Never executes
    return 0.0

Rozwiązanie: Zweryfikuj kontekst biznesowy, a następnie całkowicie usuń nieaktywną gałąź. Reguła biznesowa, która umożliwiła wystąpienie tego warunku, uległa zmianie. Kod, który ją implementował, nie jest już potrzebny. Udokumentuj usunięcie w komunikacie zatwierdzenia: „Usunięto gałąź o stawce BRONZE, poziom wyeliminowany w drugim kwartale 2021 r., wszyscy klienci przeszli na poziom SILVER”.

Przyczyna główna 3: Programowanie defensywne w celu zabezpieczenia przyszłości

Niektóre warunki, które zawsze są fałszywe, są celowe. Twórca napisał je wiedząc, że warunek nie może być obecnie prawdziwy, jako zabezpieczenie na wypadek przyszłych zmian w kodzie, które mogłyby sprawić, że warunek stanie się prawdziwy.

Jawa

// SonarQube RSPEC-2583 explicitly exempts these:
final boolean DEBUG_MODE = false;
if (DEBUG_MODE) {                // Always false -- intentional
    dumpDiagnostics();           // Not unreachable in intent, only in fact
}

// Type narrowing that appears redundant
String value = getValue();
if (value == null || value.isEmpty()) {
    return;
}
// At this point, static analysis knows value is non-null and non-empty
if (value == null) {  // Always false -- already checked above
    throw new IllegalStateException(); // Defensive guard
}

SonarQube RSPEC-2583 wyraźnie wyodrębnia wyjątek: „gdy warunek jest pojedynczą końcową wartością logiczną” lub „gdy warunek jest dosłownie prawdą lub fałszem”, są one traktowane jako celowe i nie są oznaczane flagą.

Rozwiązanie: Jeśli warunkiem jest flaga debugowania lub stała w czasie kompilacji: pozostaw ją i dodaj adnotację o wyłączeniu, jeśli narzędzie ją zgłosi. Jeśli jest to defensywna kontrola wartości null lub ochrona zakresu, która wydaje się zbędna, ale rzeczywiście działa jako zabezpieczenie: przebuduj kod, aby jawnie określić ochronę na innym poziomie lub wyłącz ją, podając udokumentowane uzasadnienie.

Przyczyna główna 4: Fałszywe wyniki pozytywne narzędzia z konfiguracji środowiska wykonawczego

Analizatory statyczne nie widzą konfiguracji środowiska wykonawczego, zmiennych środowiskowych, refleksji, przetwarzania adnotacji ani wstrzykiwania zależności. Kod, który dla analizatora zawsze wyświetla się jako fałsz, może być dostępny poprzez mechanizm środowiska wykonawczego, którego analizator nie jest w stanie modelować.

Jawa

// IntelliJ / Inspectopedia note: "This may cause false-positives if any kind
// of code postprocessing is used, for example, if an annotation processor
// later replaces the method body with something useful."

@ConditionalOnProperty(name = "feature.experimental", havingValue = "true")
public class ExperimentalFeature {
    // This class may appear unreachable if the property is false in the
    // test configuration the analyzer uses -- but it IS reachable in prod
}

// Method contract that the analyzer cannot see
void run() {
    performAction();
    System.out.println("Done"); // IntelliJ flags as unreachable
}
@Contract("-> _") // Tells the analyzer: method does NOT always throw
static void performAction() { throw new AssertionError(); }

Rozkład: Dodaj odpowiednią adnotację kontraktu metody (@Contract dla IntelliJ, @NotNull/@Nullable W przypadku Kotlina, asercje typu (dla TypeScript), aby zapewnić analizatorowi potrzebne informacje w czasie wykonywania. Jeśli rozwiązanie oparte na adnotacjach nie jest dostępne, należy użyć mechanizmu tłumienia narzędzia z udokumentowanym uzasadnieniem.

COBOL: Gdzie zawsze fałszywe warunki ujawniają dekady martwej logiki

W języku COBOL i starszych systemach mainframe warunki zawsze fałszywe mają inny charakter niż w nowoczesnym kodzie aplikacji i inne znaczenie dla zespołów, które je konserwują lub modernizują.

Programy COBOL nie są refaktoryzowane w sposób typowy dla współczesnego kodu. Gdy zmienia się reguła biznesowa, zmienia się kod podatkowy, likwiduje się warstwę produktu, aktualizuje się wymóg regulacyjny, powszechną praktyką jest dodawanie nowej logiki bez usuwania starej. Stara gałąź EVALUATE WHEN lub IF pozostaje w źródle, chroniona warunkiem, który nigdy nie może być prawdziwy, biorąc pod uwagę obecny stan przetwarzanych danych.

kobol

       EVALUATE WS-PRODUCT-TYPE
           WHEN 'STD'
               PERFORM CALC-STANDARD-RATE
           WHEN 'PRM'
               PERFORM CALC-PREMIUM-RATE
           WHEN 'BAS'
      *        BAS product type retired June 2009
      *        All BAS customers converted to STD
      *        This branch has been dead for 15+ years
               PERFORM CALC-BASIC-RATE
           WHEN 'LEG'
      *        LEG (legacy) type eliminated in 2001 system migration
      *        Should have been removed but was left as "just in case"
               PERFORM CALC-LEGACY-RATE
           WHEN OTHER
               PERFORM HANDLE-UNKNOWN-TYPE
       END-EVALUATE.

Dwie martwe gałęzie w tej Ocenie, BAS oraz LEG, nie są błędami. Są to historyczne artefakty: reguły biznesowe, które obowiązywały w określonych momentach historii systemu i stały się trwale niedostępne po wycofaniu tych typów produktów. Nikt ich nie usunął, ponieważ nikt nie był pewien, czy nadal będą potrzebne, a koszt popełnienia błędu wydawał się wyższy niż koszt ich pozostawienia.

Co ujawniają zawsze fałszywe warunki w języku COBOL:

Martwe gałęzie reprezentują logikę biznesową, która już nie ma zastosowania. PERFORM CALC-BASIC-RATE oraz PERFORM CALC-LEGACY-RATE Same cele mogą być całymi akapitami lub sekcjami kodu COBOL, potencjalnie liczącymi setki wierszy każdy, które są w podobny sposób martwe. Warunek „zawsze fałsz” to punkt wejścia do drzewa martwego kodu, który propaguje się poprzez relacje PERFORM.

W programie COBOL składającym się ze 100 000 wierszy kodu, martwe gałęzie powstałe w wyniku wycofania reguł biznesowych mogą stanowić 20–35% całego kodu. Ten odsetek to zakres migracji, który nie wymaga konwersji, testowania ani walidacji w ramach programu modernizacji. Zidentyfikowanie go przed planowaniem migracji prowadzi do bezpośredniej redukcji zakresu, a redukcja zakresu na etapie planowania to najbardziej wartościowe zastosowanie analizy warunków zawsze fałszywych.

Drzewo decyzyjne dotyczące rozdzielczości

Always-false condition warning received
            │
            ▼
    Is this a compile-time constant
    or debug flag? (DEBUG = false, etc.)
            │
     Yes ───┼─── No
            │         │
    Intentional.      ▼
    Suppress or   Does the condition
    leave as-is.  contain a logic error?
                  (a > 10 && a < 5, etc.)
                        │
                 Yes ───┼─── No
                        │          │
                  Fix the        Has the condition
                  logic bug.     become false due to
                                 a past business rule
                                 change?
                                        │
                                 Yes ───┼─── No
                                        │          │
                              Verify,         Could a runtime
                              then remove     mechanism make
                              the dead        it reachable?
                              branch.               │
                                           Yes ──── No
                                                    │
                                             Add contract
                                             annotation.     Remove the
                                                             dead branch.

Tłumienie: kiedy i jak

Tłumienie jest właściwe, gdy stan jest rzeczywiście celowy, a narzędzie nie może uzyskać informacji o kontrakcie, aby zrozumieć przyczynę. Tłumienie nie jest właściwym sposobem na uniknięcie zbadania, co tak naprawdę oznacza ostrzeżenie.

Java (SonarQube):

Jawa

@SuppressWarnings("java:S2583")
if (DEBUG_MODE) {  // Intentional: compile-time debug flag
    dumpState();
}

Java (IntelliJ):

Jawa

//noinspection ConstantConditions
if (possiblyNull != null) {  // Analyzer cannot see the runtime injection
    process(possiblyNull);
}

Pyton (Pylint):

pyton

if False:  # pylint: disable=W0125
    # Intentional: placeholder for future implementation
    pass

C# (SonarQube):

csharp

#pragma warning disable S2589
if (IsDebugBuild) {  // Compile-time constant -- intentional
    LogDiagnostics();
}
#pragma warning restore S2589

COBOL (SMART TS XL /niestandardowy komentarz do tłumienia):

kobol

      *>> SUPPRESS UNREACHABLE: BAS branch retained for audit trail
           WHEN 'BAS'
               PERFORM CALC-BASIC-RATE

Dyscyplina tłumienia: Każdemu stłumieniu powinien towarzyszyć komentarz wyjaśniający, dlaczego warunek jest celowy, a nie tylko, że jest tłumiony. Komentarz tłumienia powinien brzmieć: // suppress unreachable jest gorsze niż brak tłumienia, ukrywa ostrzeżenie bez wyjaśnienia, dlaczego jest ono błędne. Tłumienie, które mówi // suppress: DEBUG_MODE is a compile-time constant, branch is retained for IDE visibility jest do zaakceptowania.

W jaki sposób SMART TS XL Identyfikuje zawsze fałszywe warunki w skali portfela

SMART TS XL'S statyczna analiza kodu stosuje analizę przepływu danych i sterowania w językach COBOL, JCL, Java, Python i każdym innym języku w środowisku, aby identyfikować warunki, które są statycznie udowodnione jako zawsze fałszywe, na skalę całego portfolio, w tysiącach programów jednocześnie, a nie w jednym pliku na raz w środowisku IDE dla programistów.

W przypadku portfeli COBOL oznacza to, że każda klauzula EVALUATE WHEN i każdy warunek IF w każdym programie są analizowane pod kątem znanych ograniczeń danych, które do niego wpływają. Warunki odwołujące się do kodów produktów, flag statusu lub kodów typów, których zestawy prawidłowych wartości uległy zmianie w historii programu, są oznaczane jako potencjalnie martwe gałęzie. Wynikiem jest inwentaryzacja martwego kodu: każdy program, każda martwa gałąź oraz szacowana liczba wierszy martwego kodu osiągalnych z każdego warunku zawsze fałszywego.

Analiza modernizacji starszych wersji przekształca ten inwentarz w redukcję zakresu: każda martwa gałąź jest wykluczana ze specyfikacji migracji przed rozpoczęciem jakichkolwiek działań konwersyjnych. Program, który pozornie ma 2,000 linii logiki biznesowej, może mieć 600 linii martwej logiki biznesowej z wycofanych typów produktów, wyeliminowanych poziomów klientów i przestarzałych ścieżek obliczeniowych. Migracja 1,400 linii zamiast 2,000 linii to redukcja zakresu takiego programu o 30%, a martwe gałęzie stanowią najbardziej ryzykowną część zakresu migracji, ponieważ nikt nie wie, co miały robić od początku.

Funkcja wyszukiwania korporacyjnego umożliwia wyszukiwanie w inwentarzu zawsze fałszywych kodów: znajdź każdy program COBOL zawierający klauzulę EVALUATE WHEN odwołującą się do kodu produktu, którego nie ma w bieżącym katalogu produktów, każdy program sprawdzający wartość flagi statusu, która nie istniała w bazie danych od określonego roku, każdy warunek IF odwołujący się do pola struktury danych, które zostało zmienione od momentu utworzenia warunku. Zapytania te generują docelowy inwentarz martwego kodu, którego wygenerowanie w dużym portfolio COBOL zajęłoby miesiące w wyniku ręcznego przeglądu kodu.

Ostrzeżenie jest zazwyczaj słuszne

Ostrzeżenia analizy statycznej dotyczące warunków zawsze fałszywych są w większości przypadków trafnymi ustaleniami. Warunek jest zawsze fałszywy. Gałąź nigdy się nie wykonuje. Kod w niej zawarty jest martwy.

Właściwą reakcją nie jest tłumienie, lecz badanie. Która przyczyna źródłowa ma tu zastosowanie? Sprzeczność logiczna, skumulowana zmiana reguł biznesowych, celowe programowanie defensywne czy fałszywie dodatni wynik narzędzia w konfiguracji środowiska wykonawczego? Każda z nich ma poprawne rozwiązanie. Trzy z czterech prowadzą do usunięcia martwego kodu. Jedna prowadzi do tłumienia z uzasadnieniem.

W przypadku starszych systemów COBOL analiza stanu zawsze fałszywego w skali portfolio nie jest przede wszystkim ćwiczeniem z zakresu jakości kodu, lecz ćwiczeniem z zakresu redukcji zakresu. Martwe gałęzie, które kumulują się przez dekady zmian reguł biznesowych, reprezentują wysiłek migracyjny, którego nie należy realizować. Znalezienie ich przed zablokowaniem planu migracji to najbardziej wartościowe zastosowanie analizy stanu zawsze fałszywego dla każdego zespołu planującego program modernizacji COBOL.

Analizator mówi ci coś prawdziwego. Posłuchaj go.