Skip to content

Commit b04a412

Browse files
authored
warn about unnecessary uses of .nn (#23327)
Issue warnings when `.nn` is called on a receiver that is already not null or when the expected type of the result of `.nn` admits null.
1 parent 4ec6751 commit b04a412

File tree

18 files changed

+75
-27
lines changed

18 files changed

+75
-27
lines changed

compiler/src/dotty/tools/dotc/core/OrderingConstraint.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ object OrderingConstraint {
7373
}
7474
else {
7575
val prev_es = entries(prev, poly)
76-
if (prev_es == null || (es.nn ne prev_es.nn))
76+
if (prev_es == null || (es.nn ne prev_es))
7777
current // can re-use existing entries array.
7878
else {
7979
es = es.nn.clone

compiler/src/dotty/tools/dotc/core/StdNames.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,7 @@ object StdNames {
563563
val next: N = "next"
564564
val nmeNewTermName: N = "newTermName"
565565
val nmeNewTypeName: N = "newTypeName"
566+
val nn: N = "nn"
566567
val noAutoTupling: N = "noAutoTupling"
567568
val normalize: N = "normalize"
568569
val notifyAll_ : N = "notifyAll"

compiler/src/dotty/tools/dotc/core/SymDenotations.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2586,7 +2586,7 @@ object SymDenotations {
25862586
try f.container == chosen.container catch case NonFatal(ex) => true
25872587
if !ambiguityWarningIssued then
25882588
for conflicting <- assocFiles.find(!sameContainer(_)) do
2589-
report.warning(em"""${ambiguousFilesMsg(conflicting.nn)}
2589+
report.warning(em"""${ambiguousFilesMsg(conflicting)}
25902590
|Keeping only the definition in $chosen""")
25912591
ambiguityWarningIssued = true
25922592
multi.filterWithPredicate(_.symbol.associatedFile == chosen)

compiler/src/dotty/tools/dotc/core/Types.scala

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,15 +373,26 @@ object Types extends TypeUtils {
373373
final def isNotNull(using Context): Boolean = this match {
374374
case tp: ConstantType => tp.value.value != null
375375
case tp: FlexibleType => false
376-
case tp: ClassInfo => !tp.cls.isNullableClass && tp.cls != defn.NothingClass
376+
case tp: ClassInfo => !tp.cls.isNullableClass && !tp.isNothingType
377377
case tp: AppliedType => tp.superType.isNotNull
378-
case tp: TypeBounds => tp.lo.isNotNull
378+
case tp: TypeBounds => tp.hi.isNotNull
379379
case tp: TypeProxy => tp.underlying.isNotNull
380380
case AndType(tp1, tp2) => tp1.isNotNull || tp2.isNotNull
381381
case OrType(tp1, tp2) => tp1.isNotNull && tp2.isNotNull
382382
case _ => false
383383
}
384384

385+
/** Is `null` a value of this type? */
386+
def admitsNull(using Context): Boolean =
387+
isNullType || isAny || (this match
388+
case OrType(l, r) => r.admitsNull || l.admitsNull
389+
case AndType(l, r) => r.admitsNull && l.admitsNull
390+
case TypeBounds(lo, hi) => lo.admitsNull
391+
case FlexibleType(lo, hi) => true
392+
case tp: TypeProxy => tp.underlying.admitsNull
393+
case _ => false
394+
)
395+
385396
/** Is this type produced as a repair for an error? */
386397
final def isError(using Context): Boolean = stripTypeVar.isInstanceOf[ErrorType]
387398

compiler/src/dotty/tools/dotc/printing/Formatting.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ object Formatting {
6363

6464
class ShowImplicits4:
6565
given [X: Show]: Show[X | Null] with
66-
def show(x: X | Null) = if x == null then "null" else CtxShow(toStr(x.nn))
66+
def show(x: X | Null) = if x == null then "null" else CtxShow(toStr(x))
6767

6868
class ShowImplicits3 extends ShowImplicits4:
6969
given Show[Product] = ShowAny

compiler/src/dotty/tools/dotc/transform/Erasure.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,7 @@ object Erasure {
695695
val owner = sym.maybeOwner
696696
if defn.specialErasure.contains(owner) then
697697
assert(sym.isConstructor, s"${sym.showLocated}")
698-
defn.specialErasure(owner).nn
698+
defn.specialErasure(owner)
699699
else if defn.isSyntheticFunctionClass(owner) then
700700
defn.functionTypeErasure(owner).typeSymbol
701701
else

compiler/src/dotty/tools/dotc/transform/GenericSignatures.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ object GenericSignatures {
262262
typeParamSig(sym.name.lastPart)
263263
}
264264
else if (defn.specialErasure.contains(sym))
265-
jsig(defn.specialErasure(sym).nn.typeRef)
265+
jsig(defn.specialErasure(sym).typeRef)
266266
else if (sym == defn.UnitClass || sym == defn.BoxedUnitModule)
267267
jsig(defn.BoxedUnitClass.typeRef)
268268
else if (sym == defn.NothingClass)

compiler/src/dotty/tools/dotc/transform/LazyVals.scala

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -477,12 +477,12 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer {
477477
newSymbol(claz, offsetName(info.defs.size), Synthetic, defn.LongType).enteredAfter(this)
478478
case None =>
479479
newSymbol(claz, offsetName(0), Synthetic, defn.LongType).enteredAfter(this)
480-
offsetSymbol.nn.addAnnotation(Annotation(defn.ScalaStaticAnnot, offsetSymbol.nn.span))
480+
offsetSymbol.addAnnotation(Annotation(defn.ScalaStaticAnnot, offsetSymbol.span))
481481
val fieldTree = thizClass.select(lazyNme.RLazyVals.getDeclaredField).appliedTo(Literal(Constant(containerName.mangledString)))
482-
val offsetTree = ValDef(offsetSymbol.nn, getOffset.appliedTo(fieldTree))
482+
val offsetTree = ValDef(offsetSymbol, getOffset.appliedTo(fieldTree))
483483
val offsetInfo = appendOffsetDefs.getOrElseUpdate(claz, new OffsetInfo(Nil))
484484
offsetInfo.defs = offsetTree :: offsetInfo.defs
485-
val offset = ref(offsetSymbol.nn)
485+
val offset = ref(offsetSymbol)
486486

487487
val swapOver =
488488
This(claz)
@@ -617,31 +617,31 @@ class LazyVals extends MiniPhase with IdentityDenotTransformer {
617617
.symbol.asTerm
618618
else { // need to create a new flag
619619
offsetSymbol = newSymbol(claz, offsetById, Synthetic, defn.LongType).enteredAfter(this)
620-
offsetSymbol.nn.addAnnotation(Annotation(defn.ScalaStaticAnnot, offsetSymbol.nn.span))
620+
offsetSymbol.addAnnotation(Annotation(defn.ScalaStaticAnnot, offsetSymbol.span))
621621
val flagName = LazyBitMapName.fresh(id.toString.toTermName)
622622
val flagSymbol = newSymbol(claz, flagName, containerFlags, defn.LongType).enteredAfter(this)
623623
flag = ValDef(flagSymbol, Literal(Constant(0L)))
624624
val fieldTree = thizClass.select(lazyNme.RLazyVals.getDeclaredField).appliedTo(Literal(Constant(flagName.toString)))
625-
val offsetTree = ValDef(offsetSymbol.nn, getOffsetStatic.appliedTo(fieldTree))
625+
val offsetTree = ValDef(offsetSymbol, getOffsetStatic.appliedTo(fieldTree))
626626
info.defs = offsetTree :: info.defs
627627
}
628628

629629
case None =>
630630
offsetSymbol = newSymbol(claz, offsetName(0), Synthetic, defn.LongType).enteredAfter(this)
631-
offsetSymbol.nn.addAnnotation(Annotation(defn.ScalaStaticAnnot, offsetSymbol.nn.span))
631+
offsetSymbol.addAnnotation(Annotation(defn.ScalaStaticAnnot, offsetSymbol.span))
632632
val flagName = LazyBitMapName.fresh("0".toTermName)
633633
val flagSymbol = newSymbol(claz, flagName, containerFlags, defn.LongType).enteredAfter(this)
634634
flag = ValDef(flagSymbol, Literal(Constant(0L)))
635635
val fieldTree = thizClass.select(lazyNme.RLazyVals.getDeclaredField).appliedTo(Literal(Constant(flagName.toString)))
636-
val offsetTree = ValDef(offsetSymbol.nn, getOffsetStatic.appliedTo(fieldTree))
636+
val offsetTree = ValDef(offsetSymbol, getOffsetStatic.appliedTo(fieldTree))
637637
appendOffsetDefs += (claz -> new OffsetInfo(List(offsetTree), ord))
638638
}
639639

640640
val containerName = LazyLocalName.fresh(x.name.asTermName)
641641
val containerSymbol = newSymbol(claz, containerName, x.symbol.flags &~ containerFlagsMask | containerFlags, tpe, coord = x.symbol.coord).enteredAfter(this)
642642
val containerTree = ValDef(containerSymbol, defaultValue(tpe))
643643

644-
val offset = ref(offsetSymbol.nn)
644+
val offset = ref(offsetSymbol)
645645
val getFlag = Select(ref(defn.LazyValsModule), lazyNme.RLazyVals.get)
646646
val setFlag = Select(ref(defn.LazyValsModule), lazyNme.RLazyVals.setFlag)
647647
val wait = Select(ref(defn.LazyValsModule), lazyNme.RLazyVals.wait4Notification)

compiler/src/dotty/tools/dotc/typer/ProtoTypes.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -466,13 +466,13 @@ object ProtoTypes {
466466
targ = typerFn(arg)
467467
// TODO: investigate why flow typing is not working on `targ`
468468
if ctx.reporter.hasUnreportedErrors then
469-
if hasInnerErrors(targ.nn, argType) then
469+
if hasInnerErrors(targ, argType) then
470470
state.errorArgs += arg
471471
else
472-
state.typedArg = state.typedArg.updated(arg, targ.nn)
472+
state.typedArg = state.typedArg.updated(arg, targ)
473473
state.errorArgs -= arg
474474
}
475-
targ.nn
475+
targ
476476
}
477477

478478
/** The typed arguments. This takes any arguments already typed using

compiler/src/dotty/tools/dotc/typer/Typer.scala

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1079,7 +1079,19 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
10791079
errorTree(tree, em"cannot convert to type selection") // will never be printed due to fallback
10801080
}
10811081

1082-
if (tree.qualifier.isType) {
1082+
def warnUnnecessaryNN(tree: Tree): Unit = {
1083+
if ctx.explicitNulls then {
1084+
val symbol = tree.symbol
1085+
if symbol.exists && symbol.owner == defn.ScalaPredefModuleClass && symbol.name == nme.nn then
1086+
tree match
1087+
case Apply(_, args) =>
1088+
if(args.head.tpe.isNotNull) then report.warning("Unnecessary .nn: qualifier is already not null", tree)
1089+
if pt.admitsNull then report.warning("Unnecessary .nn: expected type admits null", tree)
1090+
case _ =>
1091+
}
1092+
}
1093+
1094+
val tree1 = if (tree.qualifier.isType) {
10831095
val qual1 = typedType(tree.qualifier, shallowSelectionProto(tree.name, pt, this, tree.nameSpan))
10841096
assignType(cpy.Select(tree)(qual1, tree.name), qual1)
10851097
}
@@ -1089,6 +1101,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
10891101
tryAlternatively(typeSelectOnTerm)(tryJavaSelectOnType)
10901102
else
10911103
typeSelectOnTerm
1104+
1105+
warnUnnecessaryNN(tree1)
1106+
tree1
10921107
}
10931108

10941109
def typedThis(tree: untpd.This)(using Context): Tree = {

presentation-compiler/src/main/dotty/tools/pc/CompilerSearchVisitor.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class CompilerSearchVisitor(
1919
)(using ctx: Context, reports: ReportContext)
2020
extends SymbolSearchVisitor:
2121

22-
val logger: Logger = Logger.getLogger(classOf[CompilerSearchVisitor].getName().nn).nn
22+
val logger: Logger = Logger.getLogger(classOf[CompilerSearchVisitor].getName()).nn
2323

2424
private def isAccessibleImplicitClass(sym: Symbol) =
2525
val owner = sym.maybeOwner

presentation-compiler/src/main/dotty/tools/pc/SignatureHelpProvider.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ object SignatureHelpProvider:
8484
case Some(paramDoc) =>
8585
val newName =
8686
if isJavaSymbol && head.name.startsWith("x$") then
87-
paramDoc.nn.displayName()
87+
paramDoc.displayName()
8888
else head.name
8989
head.copy(name = newName.nn, doc = Some(paramDoc.docstring.nn)) :: rest
9090
case _ => head :: rest

presentation-compiler/src/main/dotty/tools/pc/completions/CompletionPos.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ object CompletionPos:
6262
CompletionPos(
6363
start,
6464
identEnd,
65-
query.nn,
65+
query,
6666
sourcePos,
6767
offsetParams.uri.nn,
6868
wasCursorApplied,

presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ class CompletionProvider(
174174
*/
175175
private def applyCompletionCursor(params: OffsetParams): (Boolean, String) =
176176
val text = params.text().nn
177-
val offset = params.offset().nn
177+
val offset = params.offset()
178178
val query = Completion.naiveCompletionPrefix(text, offset)
179179

180180
if offset > 0 && text.charAt(offset - 1).isUnicodeIdentifierPart

presentation-compiler/test/dotty/tools/pc/utils/TestInlayHints.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ object TestInlayHints {
3636
InlayHints.fromData(inlayHint.getData().asInstanceOf[JsonElement])._2
3737
buffer += "/*"
3838
labels.zip(data).foreach { case (label, data) =>
39-
buffer += label.nn
39+
buffer += label
4040
buffer ++= readData(data)
4141
}
4242
buffer += "*/"

tests/explicit-nulls/warn/flow-match.check

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@
1111
| ^^^^
1212
| Unreachable case
1313
-- [E030] Match case Unreachable Warning: tests/explicit-nulls/warn/flow-match.scala:14:9 ------------------------------
14-
14 | case s4 => s4.nn // warn
14+
14 | case s4 => s4 // warn
1515
| ^^
1616
| Unreachable case

tests/explicit-nulls/warn/flow-match.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
object MatchTest2 {
44
def f6(s: String | Null): String = s match {
5-
case s2 => s2.nn
5+
case s2 => "string"
66
case null => "other" // warn
77
case s3 => s3 // warn
88
}
@@ -11,6 +11,6 @@ object MatchTest2 {
1111
case null => "other"
1212
case null => "other" // warn
1313
case s3: String => s3
14-
case s4 => s4.nn // warn
14+
case s4 => s4 // warn
1515
}
1616
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
def f1(s: String): String = s.nn // warn
2+
def f2(s: String|Null): String|Null = s.nn // warn
3+
def f3(s: String|Null): Any = s.nn // warn
4+
def f4(s: String|Null): String = s.nn
5+
6+
def f5[T >: String](s: String|Null): T = s.nn
7+
def f6[T >: String|Null](s: String|Null): T = s.nn // warn
8+
9+
def f5a[T <: String](s: T): String = s.nn // warn
10+
11+
// flexible types
12+
def f7(s: String|Null) = "".concat(s.nn) // warn
13+
def f8(s: String): String = s.trim().nn // OK because the .nn could be useful as a dynamic null check
14+
15+
16+
def f9(s: String|Null): String =
17+
if(s == null) "default"
18+
else s.nn // warn
19+
20+
def f10(x: String|Null): ((String|Null) @deprecated) = x.nn // warn
21+
def f10b(x: String|Null): (String|Null) = x.nn // warn

0 commit comments

Comments
 (0)