Risolvere gli avvisi di analisi statica per condizioni sempre false

Come risolvere gli avvisi di analisi statica relativi a condizioni sempre false e rami irraggiungibili

Il tuo analizzatore statico ha segnalato una condizione come sempre falsa. L'avviso si presenta in uno di questi modi, a seconda dello strumento utilizzato:

  • SonarQube: "Le espressioni condizionali che sono sempre vere o false possono portare a codice irraggiungibile." (RSPEC-2583)
  • IntelliJ / Inspectpedia: "Il codice non viene analizzato perché la condizione viene valutata staticamente come falsa."
  • Checkmarx / Fortify: "Codice irraggiungibile, rilevato ramo morto"
  • ESLint: “nessuna condizione costante, condizione costante inattesa”
  • Pylint: “W0125: Utilizzo di un'istruzione condizionale con un valore costante”

Tutti questi elementi indicano la stessa cosa: l'analizzatore ha dimostrato, in fase di analisi e senza eseguire il codice, che una specifica porzione di un'istruzione condizionale non può mai essere eseguita. La condizione che protegge tale porzione è sempre falsa. Tutto ciò che si trova all'interno di quella porzione è codice morto.

Questa guida illustra le cause di questi avvisi, come identificare la categoria di appartenenza di ciascun avviso, la soluzione corretta per ogni categoria, quando è opportuno sopprimerli e cosa rivelano le condizioni sempre false nel codice COBOL legacy in merito all'ambito della migrazione.

Regole aziendali obsolete ancora presenti nel tuo codice

SMART TS XL Identifica i rami EVALUATE che fanno riferimento a codici prodotto, valori di stato e tipologie di clienti che non esistono più nei dati di produzione.

SCOPRI DI PIÙ…

Come l'analisi statica rileva le condizioni sempre false

I moderni analizzatori statici utilizzano l'esecuzione simbolica e l'analisi del flusso di dati, non la semplice corrispondenza di pattern, per rilevare condizioni sempre false. L'analizzatore tiene traccia dei possibili valori di ciascuna variabile in ogni punto del programma. Quando raggiunge una condizione, valuta se la condizione può mai essere vera, dati i vincoli noti sulle variabili.

Tre meccanismi principali producono sempre falsi positivi:

Esecuzione simbolica. L'analizzatore assegna valori simbolici alle variabili e tiene traccia di come le operazioni vincolano tali valori. Se la variabile x è assegnato 0 sulla riga 3 e confrontato x > 10 Alla riga 7, senza alcuna assegnazione intermedia, la condizione è dimostrabilmente falsa.

Vincoli del sistema di tipi. Alcune condizioni sono impossibili dato il tipo della variabile. Confrontare un riferimento a un oggetto non nullo con null dopo un'asserzione non nulla, oppure confrontare un valore di un'enumerazione con un caso al di fuori del suo insieme definito.

Semplificazione dell'algebra booleana. Condizioni composte che semplificano in false indipendentemente dai valori delle variabili: (a && !a), (x > 10 && x < 5), (!flag || false).

La regola RSPEC-2583 di SonarQube utilizza specificamente l'esecuzione simbolica: traccia lo stato delle variabili attraverso il grafo del flusso di controllo e segnala le condizioni in cui lo stato simbolico rende la condizione dimostrabilmente falsa. L'avviso di IntelliJ "condizione valutata staticamente come falsa" utilizza lo stesso approccio.

Le quattro cause principali

La risoluzione corretta di un avviso sempre falso dipende dall'identificazione corretta di quale delle quattro cause principali si applica. La soluzione è diversa per ciascuna.

Causa principale 1: Contraddizione logica

La condizione contiene un'impossibilità logica, due sottocondizioni che non possono essere entrambe vere contemporaneamente, oppure una condizione composta che si riduce a false tramite algebra booleana.

Giava

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

Risoluzione: Correggi la logica. O la condizione contiene un errore di battitura (lo sviluppatore intendeva score > 90 || score < 50) oppure il ramo deve essere rimosso. Nell'esempio di controllo dell'intervallo, l'interno if è ridondante e dovrebbe essere eliminato.

Causa principale 2: Accumulo di modifiche alle regole aziendali

Questa è la causa più significativa nei codebase di lunga durata. Una condizione che era valida e significativa al momento della sua stesura è diventata permanentemente falsa a causa di successive modifiche al codice circostante, senza che nessuno abbia rimosso il ramo ormai obsoleto.

Giava

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

python

# 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

Risoluzione: Verificare il contesto aziendale, quindi rimuovere completamente il ramo inattivo. La regola aziendale che rendeva possibile la condizione è cambiata. Il codice che la implementava non è più necessario. Documentare la rimozione nel messaggio di commit: "Rimosso il ramo tariffa BRONZE, livello eliminato nel secondo trimestre del 2021, tutti i clienti migrati a SILVER."

Causa principale 3: Programmazione difensiva per garantire la sostenibilità futura

Alcune condizioni sempre false sono intenzionali: lo sviluppatore le ha scritte sapendo che al momento la condizione non può essere vera, come misura di sicurezza contro future modifiche al codice che potrebbero renderla tale.

Giava

// 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 prevede esplicitamente un'eccezione: "Quando la condizione è un singolo valore booleano finale" o "quando la condizione è letteralmente vera o falsa", questi casi vengono considerati intenzionali e non segnalati.

Risoluzione: Se la condizione è un flag di debug o una costante in fase di compilazione: lasciarla e aggiungere un'annotazione di soppressione se lo strumento la segnala. Se si tratta di un controllo difensivo contro i valori nulli o di una protezione contro gli intervalli che sembra ridondante ma è effettivamente protettiva: ristrutturare il codice per rendere esplicita la protezione a un livello diverso, oppure sopprimerla con una giustificazione documentata.

Causa principale 4: Falsi positivi dello strumento derivanti dalla configurazione in fase di esecuzione

Gli analizzatori statici non possono visualizzare la configurazione di runtime, le variabili d'ambiente, la reflection, l'elaborazione delle annotazioni o l'iniezione delle dipendenze. Il codice che appare sempre falso all'analizzatore potrebbe essere raggiungibile tramite un meccanismo di runtime che l'analizzatore non è in grado di modellare.

Giava

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

Risoluzione: Aggiungere l'annotazione del contratto del metodo appropriato (@Contract per IntelliJ, @NotNull/@Nullable Per Kotlin, asserzioni di tipo per TypeScript) per fornire all'analizzatore le informazioni di runtime necessarie. Se la risoluzione basata su annotazioni non è disponibile, utilizzare il meccanismo di soppressione dello strumento con una giustificazione documentata.

COBOL: dove le condizioni sempre false rivelano decenni di logica morta

Nei sistemi COBOL e nei mainframe legacy, le condizioni sempre false hanno una natura diversa rispetto al codice applicativo moderno e un significato diverso per i team che si occupano della loro manutenzione o modernizzazione.

I programmi COBOL non vengono rifattorizzati come il codice moderno. Quando cambia una regola aziendale, viene rivisto un codice fiscale, viene eliminato un livello di prodotto, viene aggiornato un requisito normativo, la prassi comune è quella di aggiungere la nuova logica senza rimuovere la vecchia. Il vecchio ramo EVALUATE WHEN o IF rimane nel codice sorgente, protetto da una condizione che non potrà mai essere vera dato lo stato attuale dei dati che elabora.

cobolo

       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.

I due rami morti in questa VALUTAZIONE, BAS and LEGNon si tratta di errori. Sono reperti storici: regole aziendali valide in specifici momenti della storia del sistema e diventate definitivamente irraggiungibili quando quei tipi di prodotto sono stati dismessi. Nessuno le ha rimosse perché nessuno era certo che potessero essere ancora necessarie, e il costo di un eventuale errore sembrava superiore al costo di lasciarle in vigore.

Cosa rivelano le condizioni sempre false in COBOL:

I rami morti rappresentano la logica aziendale che non è più applicabile. PERFORM CALC-BASIC-RATE and PERFORM CALC-LEGACY-RATE I target possono essere a loro volta interi paragrafi o sezioni di codice COBOL, potenzialmente centinaia di righe ciascuno, anch'essi inutilizzati. La condizione sempre falsa è il punto di ingresso di un albero di codice inutilizzato che si propaga attraverso le relazioni PERFORM.

In un programma COBOL di 100,000 righe, i rami morti derivanti da regole aziendali obsolete possono rappresentare il 20-35% del codice totale. Tale percentuale costituisce un ambito di migrazione che non necessita di essere convertito, testato o convalidato in un programma di modernizzazione. Identificarlo prima della pianificazione della migrazione produce una riduzione diretta dell'ambito, e la riduzione dell'ambito in fase di pianificazione è l'applicazione di maggior valore dell'analisi delle condizioni sempre false.

L'albero decisionale di risoluzione

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.

Soppressione: quando e come

La soppressione è appropriata quando la condizione è effettivamente intenzionale e allo strumento non possono essere fornite le informazioni contrattuali necessarie per comprenderne il motivo. La soppressione non è appropriata come scorciatoia per evitare di indagare sul significato effettivo dell'avviso.

Java (SonarQube):

Giava

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

Java (IntelliJ):

Giava

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

Python (Pylint):

python

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

C# (SonarQube):

nitido

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

COBOL (SMART TS XL / commento di soppressione personalizzato):

cobolo

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

Disciplina di repressione: Ogni soppressione dovrebbe essere accompagnata da un commento che spieghi perché la condizione è intenzionale, non solo che viene soppressa. Un commento di soppressione che dice // suppress unreachable è peggio di nessuna soppressione, nasconde l'avvertimento senza spiegare perché l'avvertimento è sbagliato. Una soppressione che dice // suppress: DEBUG_MODE is a compile-time constant, branch is retained for IDE visibility è accettabile.

Come SMART TS XL Identifica le condizioni sempre false a livello di portafoglio

SMART TS XL'S analisi statica del codice Applica l'analisi del flusso di dati e del flusso di controllo a COBOL, JCL, Java, Python e a ogni altro linguaggio presente nell'ambiente per identificare le condizioni che sono staticamente dimostrabili come sempre false, su scala di portfolio, su migliaia di programmi simultaneamente, non un file alla volta in un IDE di sviluppo.

Nello specifico, per i portfolio COBOL, questo significa che ogni clausola EVALUATE WHEN e ogni condizione IF in ogni programma vengono analizzate rispetto ai vincoli noti sui dati che vi confluiscono. Le condizioni che fanno riferimento a codici prodotto, flag di stato o codici tipo i cui set di valori validi sono cambiati nel corso della cronologia del programma vengono segnalate come potenziali rami morti. L'output è un inventario del codice morto: ogni programma, ogni ramo morto e il numero stimato di righe di codice morto raggiungibili da ogni condizione sempre falsa.

L' analisi di modernizzazione dei sistemi legacy converte questo inventario in una riduzione dell'ambito: ogni ramo obsoleto viene escluso dalle specifiche di migrazione prima dell'inizio di qualsiasi attività di conversione. Un programma che apparentemente ha 2,000 righe di logica di business potrebbe averne 600 di logica obsoleta derivante da tipi di prodotto dismessi, livelli di clienti eliminati e percorsi di calcolo deprecati. Migrare 1,400 righe anziché 2,000 rappresenta una riduzione del 30% dell'ambito di quel programma, e i rami obsoleti sono la parte a più alto rischio da migrare, perché nessuno sa quale fosse la loro funzione originaria.

La funzionalità di ricerca aziendale rende interrogabile l'inventario dei codici obsoleti: è possibile trovare ogni programma COBOL che contiene una clausola EVALUATE WHEN che fa riferimento a un codice prodotto non presente nel catalogo prodotti corrente, ogni programma che verifica un valore di flag di stato che non esiste nel database da un anno specifico, ogni condizione IF che fa riferimento a un campo di una struttura dati che è stato riutilizzato da quando la condizione è stata scritta. Queste query producono l'inventario mirato del codice obsoleto che una revisione manuale del codice richiederebbe mesi per essere generata in un ampio portfolio COBOL.

L'avvertimento di solito ha ragione

Gli avvisi dell'analisi statica relativi a condizioni sempre false sono, nella maggior parte dei casi, corretti. La condizione è sempre falsa. Il ramo non viene mai eseguito. Il codice al suo interno è inutilizzabile.

La risposta appropriata non è la soppressione, bensì l'indagine. Qual è la causa principale? Contraddizione logica, modifiche cumulative alle regole aziendali, programmazione difensiva intenzionale o falso positivo dello strumento dovuto alla configurazione in fase di esecuzione? Ognuna di queste ha una soluzione corretta. Tre delle quattro portano alla rimozione del codice obsoleto. Una porta alla soppressione, se giustificata.

Per i sistemi COBOL legacy, l'analisi delle condizioni sempre false su scala di portfolio non è principalmente un esercizio di qualità del codice, bensì un esercizio di riduzione dell'ambito. I rami morti che si accumulano nel corso di decenni di modifiche alle regole aziendali rappresentano uno sforzo di migrazione che non dovrebbe essere profuso. Individuarli prima che il piano di migrazione venga finalizzato è l'applicazione di maggior valore dell'analisi delle condizioni sempre false per qualsiasi team che pianifichi un programma di modernizzazione COBOL.

L'analizzatore ti sta dicendo qualcosa di vero. Ascoltalo.