Skip to content

ECJ Annotation‐based Null Analysis

Stephan Herrmann edited this page Oct 25, 2024 · 1 revision

How is annotation-based null analysis implemented in ECJ?

Using declaration annotations

When using declaration annotations (i.e., not for target TYPE_USE), the semantics of null annotations will be encoded in bindings of the affected element using bitset tagBits. This happens for:

  • locals
  • fields
  • methods, meaning: their return type

A method, however, has no binding for the parameters it declares. MethodBinding.parameters only holds the parameter-types. For that reason, such information is encoded in MethodBinding.parameterFlowBits.

Using type annotations

During resolution we compute the specified or derived nullness into each ReferenceBinding, which applies cloning to distinguish type bindings that differ only in annotations.

Responsibility for checking

Different kinds of AST nodes are handled by different utility methods:

NullAnnotationMatching.checkAssignment() handles various forms of assignment contexts:

  • Assignment
  • LocalDeclaration
  • FieldDeclaration
  • ForeachStatement
  • The resources of a TryStatement are implicitly handled as those are encoded as LocalDeclaration

Statement.analyseArguments() handles this invocation contexts:

  • MessageSend
  • AllocationExpression
  • QualifiedAllocationExpression
  • ExplicitContructorCall

Statement.checkAgainstNullTypeAnnotation() is used for partially unclear purpose in:

  • ArrayReference -- ??
  • ArrayInitializer -- actually a kind of assignment context
  • ReturnStatement -- actually a kind of assignment context

Each of the above methods is able to drill into complex expressions, to analyse all result expressions in:

  • ConditionalExpression
  • SwitchExpression

These expressions can also be freely nested. They transparently propagate the enclosing expression context.

Expression.checkNPE() is used to check nullness of receivers or similar expressions of:

  • MessageSend
  • FieldReference
  • ArrayReference
  • ReferenceExpression
  • ForeachStatement -- for its collection, as the receiver of the iterator() call
  • SwitchStatement -- if null-hostile for its switch expression (subject to conversions like unboxing)
  • SynchronizedStatement -- for its expression which is required to be nonnull by the vm
  • ThrowStatement -- cannot throw null
  • CastExpression -- really?
  • OperatorExpressions: UnaryExpression, BinaryExpression, CombinedBinaryExpression -- why?
  • CompoundAssignment -- for its lhs, why?

What about QualifiedAllocationExpression?

Expression.checkNPEbyUnboxing() checks expression for unboxing of a value that is not null-safe:

  • Invocation arguments of
    • MessageSend
    • ExplicitConstructorCall
    • AllocationExpression
    • QualifiedAllocationExpression
  • Dimensions / index of
    • ArrayAllocationExpression
    • ArrayReference
  • Single expresion of
    • CastExpression
    • Assignment
    • CompoundAssignment
    • AssertStatement
    • ReturnStatement
    • YieldStatement
  • Operands of
    • ConditionalExpression
    • OR_OR_Expression
    • AND_AND_Expression
    • check if EQUAL_EQUAL_Expression is safe, does it ever unbox?
  • Condition of
    • DoStatement
    • ForStatement
    • IfStatement
    • WhileStatement
  • Initialization of
    • LocalDeclaration
    • FieldDeclaration

T.B.C.