Skip to content
  • Benjamin Franzke's avatar
    [TASK] Add phpstan check for unneeded pseudo uncertain instanceof usage · 6c0b5518
    Benjamin Franzke authored
    An `instanceof Type` on `Type|null` is unneeded and is to be replaced by
    a null-check (or modern alternatives like optional chaning or the null
    coalescing operator) in order to avoid narrowing code branches
    unnecessarily. We call them "pseudo" uncertain checks there is no need
    to express uncertainty regarding the type in a condition where native
    type declarations define a specific type *or* null:
    
      It is `null` or `!null`.
    
    Definition of a pseudo uncertain instanceof check:
    
      `$foo instanceof Bar` is fully equivalent to `$foo !== null`,
        when `$foo` is defined (via native PHP types) to be `Bar|null`.
       ⇒ `instanceof` expresses pseudo uncertainty regarding the type.
    
    From what we have seen in previous gerrit discussions, there were two
    reasons why instanceof was preferred over null checks although being
    unneeded:
    
    1) Cognitive load for an instanceof check is perceived to be lower in
       contrast to negated null (not null) conditions
    2) Preparatory safe-guard against type of $foo being changed at sometime
       later
    
    1) Cognitive load is a subjective term and the opinions actually differ
       a lot. Some developers prefer narrowing instanceof conditions because
       they claim the desired type for a certain code branch.
       Some others say that's a clear signal for code that needs refactoring
       and perceive a high cognitive load because they do not understand why
       the type is unnecessarily checked if it can only be null or not null.
       Lets call that: "reverse cognitive load".
       That means, this argument basically boils down to
       "congitive load" (for the good "then" case: inner code block) vs
       "reverse cognitive load" (for the bad "else" case: outer code block)
       ⇒ Due to being subjective "cognitive load" is not a good argument to
         base a decision upon.
    
    2) The second argument is that an instanceof ensures a method that is to
       be called actually exists and doesn't lead to an error – that is a
       "preparatory safe-guard".
       This is true and works, but doesn't "answer" the question,
       what happens if the object is not an instance of the desired type
       (but not null).
       While preparatory safe-guards against the type of variable being
       changed sometime later was probably a pretty good idea for code that
       is not statically analyzed and had no native type declarations, but
       such checks effectively preclude that the type must/should never
       change (which might not be true!) and has no chance of actually
       detecting when that case (type change/extension) ever happens.
       All advantages offered by pseudo uncertain instanceof checks are
       accomplished with static code analysis as well, but with the added
       downside that an `instanceof` hardcodes our human static code
       analysis result, instead of letting the static analyzer do the job.
    
       To explain that:
       If the type of the variable under test is actually widened (like a
       union type, or change to a base class), it will never be
       automatically detected that there is an instanceof condition that
       restricts the type too narrowly. It will always be valid code from
       static code analysis perspective.
    
       In comparison to that, static analysis on null-checked variables will
       report invalid method calls or assignments not allowed by the
       (natively defined) types and will notify in case a type change
       requires the code to be adapted.
       We gain the advantage that the code will not be forgotten to
       be updated to a new type.
    
       That means !== null combined with static code analysis has the same
       level of being a safeguard against the bad cases, while instanceof
       silently transforms new "good"-cases into bugs, where !== null is a
       transparent and secure passthrough.
    
       Actually to make an uncertain instanceof robust, an elseif branch
       would be needed to be somehow notified about new good-cases without
       silently ignoring them:
    
       if ($foo instanceof Foo) {
           …
       } elseif ($foo !== null) {
           throw new MustNeverHappenException(…);
       }
    
    In other words an unneeded pseudo uncertain instanceof check is
    basically like a switch construct without a default case.
    Just to be explicit: Of course, instanceof is fine to be used
    when multiples types are to be expected and handled in different code
    branches.
    
    That means pseudo uncertain instanceof usage instead of null-checks is
    an antipattern for the following reasons:
    
     * It narrow code branches for the sake of less cognitive load
       * The cognitive load appears to be lower, but actually future-bad
         cases are overseen and are never auto-detectable in future – while
         null-checks will resolve to static analysis errors in case the
         input type is *ever* widened (which uncertain `instanceof` checks
         try to prepare for, but actually introduce future-bugs because of
         missing `else` cases)
     * It embraces deep nesting instead of early returns via null-checks
     * It embraces conditions over newer and more elegant PHP techniques
       like optional chaing
     * Tries to "help" the developer by explicitly anotating the
       current type of the variable under test
       ⇒ This is a clear sign of code smell, that needs to refactored into
          smaller chunks and methods and type autocompletion/information
          can be provided by IDEs when using proper types (which this change
          is about) anyway
     * Has zero advantages over static code analysis
    
    Resolves: #102140
    Releases: main, 12.4
    Change-Id: I10de41e9744a814c9e24255573b5a5eaf6fb8b0f
    Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/80859
    
    
    Tested-by: default avatarAndreas Kienast <a.fernandez@scripting-base.de>
    Tested-by: default avatarcore-ci <typo3@b13.com>
    Tested-by: default avatarOliver Hader <oliver.hader@typo3.org>
    Reviewed-by: default avatarOliver Hader <oliver.hader@typo3.org>
    Tested-by: default avatarBenjamin Franzke <ben@bnf.dev>
    Reviewed-by: default avatarBenjamin Franzke <ben@bnf.dev>
    Reviewed-by: default avatarAndreas Kienast <a.fernandez@scripting-base.de>
    Reviewed-by: default avatarNikita Hovratov <nikita.h@live.de>
    6c0b5518