/** * @name Serializable inner class of non-serializable class * @description A class that is serializable with an enclosing class that is not serializable * causes serialization to fail. * @kind problem * @problem.severity warning * @precision medium * @id java/non-serializable-inner-class * @tags reliability * maintainability * language-features */ import java predicate isSerializable(RefType t) { t.getAnAncestor() instanceof TypeSerializable } predicate withinStaticContext(NestedClass c) { c.isStatic() or c.(AnonymousClass).getClassInstanceExpr().getEnclosingCallable().isStatic() // JLS 15.9.2 } RefType enclosingInstanceType(Class inner) { not withinStaticContext(inner) and result = inner.(NestedClass).getEnclosingType() } predicate castTo(ClassInstanceExpr cie, RefType to) { exists(LocalVariableDeclExpr lvd | lvd.getInit() = cie | to = lvd.getType()) or exists(Assignment a | a.getSource() = cie | to = a.getType()) or exists(Call call, int n | call.getArgument(n) = cie | to = call.getCallee().getParameterType(n)) or exists(ReturnStmt ret | ret.getResult() = cie | to = ret.getEnclosingCallable().getReturnType()) or exists(ArrayCreationExpr ace | ace.getInit().getAnInit() = cie | to = ace.getType().(Array).getComponentType() ) } predicate exceptions(NestedClass inner) { inner instanceof AnonymousClass or // Serializable objects with custom `readObject` or `writeObject` methods may write out // the "non-serializable" fields in a different way. inner.declaresMethod("readObject") or inner.declaresMethod("writeObject") or // Exclude cases where serialization warnings are deliberately suppressed. inner.suppressesWarningsAbout("serial") or // The class `inner` is a local class or non-public member class and // all its instance expressions are cast to non-serializable types. (inner instanceof LocalClass or not inner.isPublic()) and forall(ClassInstanceExpr cie, RefType target | cie.getConstructedType() = inner and castTo(cie, target) | not isSerializable(target) ) and // Exception 1: the expression is used as an argument to `writeObject()`. not exists(Call writeCall, ClassInstanceExpr cie | cie.getConstructedType() = inner | writeCall.getCallee().hasName("writeObject") and writeCall.getAnArgument() = cie ) and // Exception 2: the expression is thrown as an exception (exceptions should be serializable // due to use cases such as remote procedure calls, logging, etc.) not exists(ThrowStmt ts, ClassInstanceExpr cie | cie.getConstructedType() = inner and ts.getExpr() = cie ) and // Exception 3: if the programmer added a `serialVersionUID`, we interpret that // as an intent to make the class serializable. not exists(Field f | f.getDeclaringType() = inner | f.hasName("serialVersionUID")) } from NestedClass inner, Class outer, string advice where inner.getFile().isJavaSourceFile() and isSerializable(inner) and outer = enclosingInstanceType+(inner) and not isSerializable(outer) and not exceptions(inner) and ( if inner instanceof LocalClass then advice = "Consider implementing readObject() and writeObject()." else advice = "Consider making the class static or implementing readObject() and writeObject()." ) select inner, "Serializable inner class of non-serializable class $@. " + advice, outer, outer.getName()