I experimented with tab sizes a bit. I took a file I had written that used exclusively indentation syntax and
rendered it with tab sizes of 2, 3, and 4. Please bear with me since the file is about 300 lines long, but that way I hope to get a more representative impression.
Nullables.scala, tab size = 2
/** Operations for implementing a flow analysis for nullability */
object Nullables:
import ast.tpd._
/** A set of val or var references that are known to be not null, plus a set of
* variable references that are not known (anymore) to be not null
*/
case class NotNullInfo(asserted: Set[TermRef], retracted: Set[TermRef]):
assert((asserted & retracted).isEmpty)
def isEmpty = this eq NotNullInfo.empty
def retractedInfo = NotNullInfo(Set(), retracted)
/** The sequential combination with another not-null info */
def seq(that: NotNullInfo): NotNullInfo =
if this.isEmpty then that
else if that.isEmpty then this
else NotNullInfo(
this.asserted.union(that.asserted).diff(that.retracted),
this.retracted.union(that.retracted).diff(that.asserted))
/** The alternative path combination with another not-null info. Used to merge
* the nullability info of the two branches of an if.
*/
def alt(that: NotNullInfo): NotNullInfo =
NotNullInfo(this.asserted.intersect(that.asserted), this.retracted.union(that.retracted))
object NotNullInfo:
val empty = new NotNullInfo(Set(), Set())
def apply(asserted: Set[TermRef], retracted: Set[TermRef]): NotNullInfo =
if asserted.isEmpty && retracted.isEmpty then empty
else new NotNullInfo(asserted, retracted)
end NotNullInfo
/** A pair of not-null sets, depending on whether a condition is `true` or `false` */
case class NotNullConditional(ifTrue: Set[TermRef], ifFalse: Set[TermRef]):
def isEmpty = this eq NotNullConditional.empty
object NotNullConditional:
val empty = new NotNullConditional(Set(), Set())
def apply(ifTrue: Set[TermRef], ifFalse: Set[TermRef]): NotNullConditional =
if ifTrue.isEmpty && ifFalse.isEmpty then empty
else new NotNullConditional(ifTrue, ifFalse)
end NotNullConditional
/** An attachment that represents conditional flow facts established
* by this tree, which represents a condition.
*/
private[typer] val NNConditional = Property.StickyKey[NotNullConditional]
/** An attachment that represents unconditional flow facts established
* by this tree.
*/
private[typer] val NNInfo = Property.StickyKey[NotNullInfo]
/** An extractor for null comparisons */
object CompareNull:
/** Matches one of
*
* tree == null, tree eq null, null == tree, null eq tree
* tree != null, tree ne null, null != tree, null ne tree
*
* The second boolean result is true for equality tests, false for inequality tests
*/
def unapply(tree: Tree)(using Context): Option[(Tree, Boolean)] = tree match
case Apply(Select(l, _), Literal(Constant(null)) :: Nil) =>
testSym(tree.symbol, l)
case Apply(Select(Literal(Constant(null)), _), r :: Nil) =>
testSym(tree.symbol, r)
case _ =>
None
private def testSym(sym: Symbol, operand: Tree)(using Context) =
if sym == defn.Any_== || sym == defn.Object_eq then Some((operand, true))
else if sym == defn.Any_!= || sym == defn.Object_ne then Some((operand, false))
else None
end CompareNull
/** An extractor for null-trackable references */
object TrackedRef:
def unapply(tree: Tree)(using Context): Option[TermRef] = tree.typeOpt match
case ref: TermRef if isTracked(ref) => Some(ref)
case _ => None
end TrackedRef
def isTracked(ref: TermRef)(using Context) =
ref.isStable
|| { val sym = ref.symbol
!ref.usedOutOfOrder
&& sym.span.exists
&& ctx.compilationUnit != null // could be null under -Ytest-pickler
&& ctx.compilationUnit.assignmentSpans.contains(sym.span.start)
}
/** The nullability context to be used after a case that matches pattern `pat`.
* If `pat` is `null`, this will assert that the selector `sel` is not null afterwards.
*/
def afterPatternContext(sel: Tree, pat: Tree)(using Context) = (sel, pat) match
case (TrackedRef(ref), Literal(Constant(null))) => ctx.addNotNullRefs(Set(ref))
case _ => ctx
/** The nullability context to be used for the guard and rhs of a case with
* given pattern `pat`. If the pattern can only match non-null values, this
* will assert that the selector `sel` is not null in these regions.
*/
def caseContext(sel: Tree, pat: Tree)(using Context): Context = sel match
case TrackedRef(ref) if matchesNotNull(pat) => ctx.addNotNullRefs(Set(ref))
case _ => ctx
private def matchesNotNull(pat: Tree)(using Context): Boolean = pat match
case _: Typed | _: UnApply => true
case Alternative(pats) => pats.forall(matchesNotNull)
// TODO: Add constant pattern if the constant type is not nullable
case _ => false
extension (infos: List[NotNullInfo])
/** Do the current not-null infos imply that `ref` is not null?
* Not-null infos are as a history where earlier assertions and retractions replace
* later ones (i.e. it records the assignment history in reverse, with most recent first)
*/
@tailrec def impliesNotNull(ref: TermRef): Boolean = infos match
case info :: infos1 =>
if info.asserted.contains(ref) then true
else if info.retracted.contains(ref) then false
else infos1.impliesNotNull(ref)
case _ =>
false
/** Add `info` as the most recent entry to the list of null infos. Assertions
* or retractions in `info` supersede infos in existing entries of `infos`.
*/
def extendWith(info: NotNullInfo) =
if info.isEmpty
|| info.asserted.forall(infos.impliesNotNull(_))
&& !info.retracted.exists(infos.impliesNotNull(_))
then infos
else info :: infos
/** Retract all references to mutable variables */
def retractMutables(using Context) =
val mutables = infos.foldLeft(Set[TermRef]())((ms, info) =>
ms.union(info.asserted.filter(_.symbol.is(Mutable))))
infos.extendWith(NotNullInfo(Set(), mutables))
end extension
extension (ref: TermRef)
def usedOutOfOrder(using Context): Boolean =
val refSym = ref.symbol
val refOwner = refSym.owner
@tailrec def recur(s: Symbol): Boolean =
s != NoSymbol
&& s != refOwner
&& (s.isOneOf(Lazy | Method) // not at the rhs of lazy ValDef or in a method (or lambda)
|| s.isClass // not in a class
// TODO: need to check by-name parameter
|| recur(s.owner))
refSym.is(Mutable) // if it is immutable, we don't need to check the rest conditions
&& refOwner.isTerm
&& recur(ctx.owner)
end extension
extension (tree: Tree)
/* The `tree` with added nullability attachment */
def withNotNullInfo(info: NotNullInfo): tree.type =
if !info.isEmpty then tree.putAttachment(NNInfo, info)
tree
/* The nullability info of `tree` */
def notNullInfo(using Context): NotNullInfo =
stripInlined(tree).getAttachment(NNInfo) match
case Some(info) if !ctx.erasedTypes => info
case _ => NotNullInfo.empty
/* The nullability info of `tree`, assuming it is a condition that evaluates to `c` */
def notNullInfoIf(c: Boolean)(using Context): NotNullInfo =
val cond = tree.notNullConditional
if cond.isEmpty then tree.notNullInfo
else tree.notNullInfo.seq(NotNullInfo(if c then cond.ifTrue else cond.ifFalse, Set()))
/** The paths that are known to be not null if the condition represented
* by `tree` yields `true` or `false`. Two empty sets if `tree` is not
* a condition.
*/
def notNullConditional(using Context): NotNullConditional =
stripBlock(tree).getAttachment(NNConditional) match
case Some(cond) if !ctx.erasedTypes => cond
case _ => NotNullConditional.empty
/** The current context augmented with nullability information of `tree` */
def nullableContext(using Context): Context =
val info = tree.notNullInfo
if info.isEmpty then ctx else ctx.addNotNullInfo(info)
/** The current context augmented with nullability information,
* assuming the result of the condition represented by `tree` is the same as
* the value of `c`.
*/
def nullableContextIf(c: Boolean)(using Context): Context =
val info = tree.notNullInfoIf(c)
if info.isEmpty then ctx else ctx.addNotNullInfo(info)
/** The context to use for the arguments of the function represented by `tree`.
* This is the current context, augmented with nullability information
* of the left argument, if the application is a boolean `&&` or `||`.
*/
def nullableInArgContext(using Context): Context = tree match
case Select(x, _) if !ctx.erasedTypes =>
if tree.symbol == defn.Boolean_&& then x.nullableContextIf(true)
else if tree.symbol == defn.Boolean_|| then x.nullableContextIf(false)
else ctx
case _ => ctx
end extension
end Nullables
Nullables.scala, tab size = 3
/** Operations for implementing a flow analysis for nullability */
object Nullables:
import ast.tpd._
/** A set of val or var references that are known to be not null, plus a set of
* variable references that are not known (anymore) to be not null
*/
case class NotNullInfo(asserted: Set[TermRef], retracted: Set[TermRef]):
assert((asserted & retracted).isEmpty)
def isEmpty = this eq NotNullInfo.empty
def retractedInfo = NotNullInfo(Set(), retracted)
/** The sequential combination with another not-null info */
def seq(that: NotNullInfo): NotNullInfo =
if this.isEmpty then that
else if that.isEmpty then this
else NotNullInfo(
this.asserted.union(that.asserted).diff(that.retracted),
this.retracted.union(that.retracted).diff(that.asserted))
/** The alternative path combination with another not-null info. Used to merge
* the nullability info of the two branches of an if.
*/
def alt(that: NotNullInfo): NotNullInfo =
NotNullInfo(this.asserted.intersect(that.asserted), this.retracted.union(that.retracted))
object NotNullInfo:
val empty = new NotNullInfo(Set(), Set())
def apply(asserted: Set[TermRef], retracted: Set[TermRef]): NotNullInfo =
if asserted.isEmpty && retracted.isEmpty then empty
else new NotNullInfo(asserted, retracted)
end NotNullInfo
/** A pair of not-null sets, depending on whether a condition is `true` or `false` */
case class NotNullConditional(ifTrue: Set[TermRef], ifFalse: Set[TermRef]):
def isEmpty = this eq NotNullConditional.empty
object NotNullConditional:
val empty = new NotNullConditional(Set(), Set())
def apply(ifTrue: Set[TermRef], ifFalse: Set[TermRef]): NotNullConditional =
if ifTrue.isEmpty && ifFalse.isEmpty then empty
else new NotNullConditional(ifTrue, ifFalse)
end NotNullConditional
/** An attachment that represents conditional flow facts established
* by this tree, which represents a condition.
*/
private[typer] val NNConditional = Property.StickyKey[NotNullConditional]
/** An attachment that represents unconditional flow facts established
* by this tree.
*/
private[typer] val NNInfo = Property.StickyKey[NotNullInfo]
/** An extractor for null comparisons */
object CompareNull:
/** Matches one of
*
* tree == null, tree eq null, null == tree, null eq tree
* tree != null, tree ne null, null != tree, null ne tree
*
* The second boolean result is true for equality tests, false for inequality tests
*/
def unapply(tree: Tree)(using Context): Option[(Tree, Boolean)] =
tree match
case Apply(Select(l, _), Literal(Constant(null)) :: Nil) =>
testSym(tree.symbol, l)
case Apply(Select(Literal(Constant(null)), _), r :: Nil) =>
testSym(tree.symbol, r)
case _ =>
None
private def testSym(sym: Symbol, operand: Tree)(using Context) =
if sym == defn.Any_== || sym == defn.Object_eq then Some((operand, true))
else if sym == defn.Any_!= || sym == defn.Object_ne then Some((operand, false))
else None
end CompareNull
/** An extractor for null-trackable references */
object TrackedRef:
def unapply(tree: Tree)(using Context): Option[TermRef] =
tree.typeOpt match
case ref: TermRef if isTracked(ref) => Some(ref)
case _ => None
end TrackedRef
def isTracked(ref: TermRef)(using Context) =
ref.isStable
|| {
val sym = ref.symbol
!ref.usedOutOfOrder
&& sym.span.exists
&& ctx.compilationUnit != null // could be null under -Ytest-pickler
&& ctx.compilationUnit.assignmentSpans.contains(sym.span.start)
}
/** The nullability context to be used after a case that matches pattern `pat`.
* If `pat` is `null`, this will assert that the selector `sel` is not null afterwards.
*/
def afterPatternContext(sel: Tree, pat: Tree)(using Context) =
(sel, pat) match
case (TrackedRef(ref), Literal(Constant(null))) => ctx.addNotNullRefs(Set(ref))
case _ => ctx
/** The nullability context to be used for the guard and rhs of a case with
* given pattern `pat`. If the pattern can only match non-null values, this
* will assert that the selector `sel` is not null in these regions.
*/
def caseContext(sel: Tree, pat: Tree)(using Context): Context =
sel match
case TrackedRef(ref) if matchesNotNull(pat) => ctx.addNotNullRefs(Set(ref))
case _ => ctx
private def matchesNotNull(pat: Tree)(using Context): Boolean =
pat match
case _: Typed | _: UnApply => true
case Alternative(pats) => pats.forall(matchesNotNull)
// TODO: Add constant pattern if the constant type is not nullable
case _ => false
extension (infos: List[NotNullInfo])
/** Do the current not-null infos imply that `ref` is not null?
* Not-null infos are as a history where earlier assertions and retractions replace
* later ones (i.e. it records the assignment history in reverse, with most recent first)
*/
@tailrec def impliesNotNull(ref: TermRef): Boolean =
infos match
case info :: infos1 =>
if info.asserted.contains(ref) then true
else if info.retracted.contains(ref) then false
else infos1.impliesNotNull(ref)
case _ =>
false
/** Add `info` as the most recent entry to the list of null infos. Assertions
* or retractions in `info` supersede infos in existing entries of `infos`.
*/
def extendWith(info: NotNullInfo) =
if info.isEmpty
|| info.asserted.forall(infos.impliesNotNull(_))
&& !info.retracted.exists(infos.impliesNotNull(_))
then infos
else info :: infos
/** Retract all references to mutable variables */
def retractMutables(using Context) =
val mutables = infos.foldLeft(Set[TermRef]())((ms, info) =>
ms.union(info.asserted.filter(_.symbol.is(Mutable))))
infos.extendWith(NotNullInfo(Set(), mutables))
end extension
extension (ref: TermRef)
def usedOutOfOrder(using Context): Boolean =
val refSym = ref.symbol
val refOwner = refSym.owner
@tailrec def recur(s: Symbol): Boolean =
s != NoSymbol
&& s != refOwner
&& (s.isOneOf(Lazy | Method) // not at the rhs of lazy ValDef or in a method (or lambda)
|| s.isClass // not in a class
// TODO: need to check by-name parameter
|| recur(s.owner)
)
refSym.is(Mutable) // if it is immutable, we don't need to check the rest conditions
&& refOwner.isTerm
&& recur(ctx.owner)
end extension
extension (tree: Tree)
/* The `tree` with added nullability attachment */
def withNotNullInfo(info: NotNullInfo): tree.type =
if !info.isEmpty then tree.putAttachment(NNInfo, info)
tree
/* The nullability info of `tree` */
def notNullInfo(using Context): NotNullInfo =
stripInlined(tree).getAttachment(NNInfo) match
case Some(info) if !ctx.erasedTypes => info
case _ => NotNullInfo.empty
/* The nullability info of `tree`, assuming it is a condition that evaluates to `c` */
def notNullInfoIf(c: Boolean)(using Context): NotNullInfo =
val cond = tree.notNullConditional
if cond.isEmpty then tree.notNullInfo
else tree.notNullInfo.seq(NotNullInfo(if c then cond.ifTrue else cond.ifFalse, Set()))
/** The paths that are known to be not null if the condition represented
* by `tree` yields `true` or `false`. Two empty sets if `tree` is not
* a condition.
*/
def notNullConditional(using Context): NotNullConditional =
stripBlock(tree).getAttachment(NNConditional) match
case Some(cond) if !ctx.erasedTypes => cond
case _ => NotNullConditional.empty
/** The current context augmented with nullability information of `tree` */
def nullableContext(using Context): Context =
val info = tree.notNullInfo
if info.isEmpty then ctx else ctx.addNotNullInfo(info)
/** The current context augmented with nullability information,
* assuming the result of the condition represented by `tree` is the same as
* the value of `c`.
*/
def nullableContextIf(c: Boolean)(using Context): Context =
val info = tree.notNullInfoIf(c)
if info.isEmpty then ctx else ctx.addNotNullInfo(info)
/** The context to use for the arguments of the function represented by `tree`.
* This is the current context, augmented with nullability information
* of the left argument, if the application is a boolean `&&` or `||`.
*/
def nullableInArgContext(using Context): Context = tree match
case Select(x, _) if !ctx.erasedTypes =>
if tree.symbol == defn.Boolean_&& then x.nullableContextIf(true)
else if tree.symbol == defn.Boolean_|| then x.nullableContextIf(false)
else ctx
case _ => ctx
end extension
end Nullables
Nullables.scala, tab size = 4
/** Operations for implementing a flow analysis for nullability */
object Nullables:
import ast.tpd._
/** A set of val or var references that are known to be not null, plus a set of
* variable references that are not known (anymore) to be not null
*/
case class NotNullInfo(asserted: Set[TermRef], retracted: Set[TermRef]):
assert((asserted & retracted).isEmpty)
def isEmpty = this eq NotNullInfo.empty
def retractedInfo = NotNullInfo(Set(), retracted)
/** The sequential combination with another not-null info */
def seq(that: NotNullInfo): NotNullInfo =
if this.isEmpty then that
else if that.isEmpty then this
else NotNullInfo(
this.asserted.union(that.asserted).diff(that.retracted),
this.retracted.union(that.retracted).diff(that.asserted))
/** The alternative path combination with another not-null info. Used to merge
* the nullability info of the two branches of an if.
*/
def alt(that: NotNullInfo): NotNullInfo =
NotNullInfo(this.asserted.intersect(that.asserted), this.retracted.union(that.retracted))
object NotNullInfo:
val empty = new NotNullInfo(Set(), Set())
def apply(asserted: Set[TermRef], retracted: Set[TermRef]): NotNullInfo =
if asserted.isEmpty && retracted.isEmpty then empty
else new NotNullInfo(asserted, retracted)
end NotNullInfo
/** A pair of not-null sets, depending on whether a condition is `true` or `false` */
case class NotNullConditional(ifTrue: Set[TermRef], ifFalse: Set[TermRef]):
def isEmpty = this eq NotNullConditional.empty
object NotNullConditional:
val empty = new NotNullConditional(Set(), Set())
def apply(ifTrue: Set[TermRef], ifFalse: Set[TermRef]): NotNullConditional =
if ifTrue.isEmpty && ifFalse.isEmpty then empty
else new NotNullConditional(ifTrue, ifFalse)
end NotNullConditional
/** An attachment that represents conditional flow facts established
* by this tree, which represents a condition.
*/
private[typer] val NNConditional = Property.StickyKey[NotNullConditional]
/** An attachment that represents unconditional flow facts established
* by this tree.
*/
private[typer] val NNInfo = Property.StickyKey[NotNullInfo]
/** An extractor for null comparisons */
object CompareNull:
/** Matches one of
*
* tree == null, tree eq null, null == tree, null eq tree
* tree != null, tree ne null, null != tree, null ne tree
*
* The second boolean result is true for equality tests, false for inequality tests
*/
def unapply(tree: Tree)(using Context): Option[(Tree, Boolean)] =
tree match
case Apply(Select(l, _), Literal(Constant(null)) :: Nil) =>
testSym(tree.symbol, l)
case Apply(Select(Literal(Constant(null)), _), r :: Nil) =>
testSym(tree.symbol, r)
case _ =>
None
private def testSym(sym: Symbol, operand: Tree)(using Context) =
if sym == defn.Any_== || sym == defn.Object_eq then Some((operand, true))
else if sym == defn.Any_!= || sym == defn.Object_ne then Some((operand, false))
else None
end CompareNull
/** An extractor for null-trackable references */
object TrackedRef:
def unapply(tree: Tree)(using Context): Option[TermRef] =
tree.typeOpt match
case ref: TermRef if isTracked(ref) => Some(ref)
case _ => None
end TrackedRef
def isTracked(ref: TermRef)(using Context) =
ref.isStable
|| {
val sym = ref.symbol
!ref.usedOutOfOrder
&& sym.span.exists
&& ctx.compilationUnit != null // could be null under -Ytest-pickler
&& ctx.compilationUnit.assignmentSpans.contains(sym.span.start)
}
/** The nullability context to be used after a case that matches pattern `pat`.
* If `pat` is `null`, this will assert that the selector `sel` is not null afterwards.
*/
def afterPatternContext(sel: Tree, pat: Tree)(using Context) =
(sel, pat) match
case (TrackedRef(ref), Literal(Constant(null))) => ctx.addNotNullRefs(Set(ref))
case _ => ctx
/** The nullability context to be used for the guard and rhs of a case with
* given pattern `pat`. If the pattern can only match non-null values, this
* will assert that the selector `sel` is not null in these regions.
*/
def caseContext(sel: Tree, pat: Tree)(using Context): Context =
sel match
case TrackedRef(ref) if matchesNotNull(pat) => ctx.addNotNullRefs(Set(ref))
case _ => ctx
private def matchesNotNull(pat: Tree)(using Context): Boolean =
pat match
case _: Typed | _: UnApply => true
case Alternative(pats) => pats.forall(matchesNotNull)
// TODO: Add constant pattern if the constant type is not nullable
case _ => false
extension (infos: List[NotNullInfo])
/** Do the current not-null infos imply that `ref` is not null?
* Not-null infos are as a history where earlier assertions and retractions replace
* later ones (i.e. it records the assignment history in reverse, with most recent first)
*/
@tailrec def impliesNotNull(ref: TermRef): Boolean =
infos match
case info :: infos1 =>
if info.asserted.contains(ref) then true
else if info.retracted.contains(ref) then false
else infos1.impliesNotNull(ref)
case _ =>
false
/** Add `info` as the most recent entry to the list of null infos. Assertions
* or retractions in `info` supersede infos in existing entries of `infos`.
*/
def extendWith(info: NotNullInfo) =
if info.isEmpty
|| info.asserted.forall(infos.impliesNotNull(_))
&& !info.retracted.exists(infos.impliesNotNull(_))
then infos
else info :: infos
/** Retract all references to mutable variables */
def retractMutables(using Context) =
val mutables = infos.foldLeft(Set[TermRef]())((ms, info) =>
ms.union(info.asserted.filter(_.symbol.is(Mutable))))
infos.extendWith(NotNullInfo(Set(), mutables))
end extension
extension (ref: TermRef)
def usedOutOfOrder(using Context): Boolean =
val refSym = ref.symbol
val refOwner = refSym.owner
@tailrec def recur(s: Symbol): Boolean =
s != NoSymbol
&& s != refOwner
&& (s.isOneOf(Lazy | Method) // not at the rhs of lazy ValDef or in a method (or lambda)
|| s.isClass // not in a class
// TODO: need to check by-name parameter
|| recur(s.owner)
)
refSym.is(Mutable) // if it is immutable, we don't need to check the rest conditions
&& refOwner.isTerm
&& recur(ctx.owner)
end extension
extension (tree: Tree)
/* The `tree` with added nullability attachment */
def withNotNullInfo(info: NotNullInfo): tree.type =
if !info.isEmpty then tree.putAttachment(NNInfo, info)
tree
/* The nullability info of `tree` */
def notNullInfo(using Context): NotNullInfo =
stripInlined(tree).getAttachment(NNInfo) match
case Some(info) if !ctx.erasedTypes => info
case _ => NotNullInfo.empty
/* The nullability info of `tree`, assuming it is a condition that evaluates to `c` */
def notNullInfoIf(c: Boolean)(using Context): NotNullInfo =
val cond = tree.notNullConditional
if cond.isEmpty then tree.notNullInfo
else tree.notNullInfo.seq(NotNullInfo(if c then cond.ifTrue else cond.ifFalse, Set()))
/** The paths that are known to be not null if the condition represented
* by `tree` yields `true` or `false`. Two empty sets if `tree` is not
* a condition.
*/
def notNullConditional(using Context): NotNullConditional =
stripBlock(tree).getAttachment(NNConditional) match
case Some(cond) if !ctx.erasedTypes => cond
case _ => NotNullConditional.empty
/** The current context augmented with nullability information of `tree` */
def nullableContext(using Context): Context =
val info = tree.notNullInfo
if info.isEmpty then ctx else ctx.addNotNullInfo(info)
/** The current context augmented with nullability information,
* assuming the result of the condition represented by `tree` is the same as
* the value of `c`.
*/
def nullableContextIf(c: Boolean)(using Context): Context =
val info = tree.notNullInfoIf(c)
if info.isEmpty then ctx else ctx.addNotNullInfo(info)
/** The context to use for the arguments of the function represented by `tree`.
* This is the current context, augmented with nullability information
* of the left argument, if the application is a boolean `&&` or `||`.
*/
def nullableInArgContext(using Context): Context = tree match
case Select(x, _) if !ctx.erasedTypes =>
if tree.symbol == defn.Boolean_&& then x.nullableContextIf(true)
else if tree.symbol == defn.Boolean_|| then x.nullableContextIf(false)
else ctx
case _ => ctx
end extension
end Nullables
To my eyes, there’s a clear winner: tabsize 3. tabsize 2 is a bit too small, and tabsize 4 feels clunky. But 3 is perfect. There are also some specific reasons why 3 is better than 4, namely:
- it does not align with
def
orval
. - it does align with
if
and&&
and||
.
You have to see for yourself why that makes a difference.
That only leaves the problem that tabsize 3 will be even harder to establish than 4 since it is so uncommon…