Repository: VKCOM/noverify
Pull request #1245 (2025-02-12 01:03:56)

linter: null safety for functions, methods and properties for classes


Patch:
diff --git a/docs/checkers_doc.md b/docs/checkers_doc.md
index 1b67701..587630d 100644
--- a/docs/checkers_doc.md
+++ b/docs/checkers_doc.md
@@ -4,7 +4,7 @@
 
 | Total checks | Checks enabled by default | Disabled checks by default | Autofixable checks |
 | ------------ | ------------------------- | -------------------------- | ------------------ |
-| 106           | 88                        | 18                         | 15                 |
+| 107           | 89                        | 18                         | 15                 |
 
 ## Table of contents
  - Enabled by default
@@ -60,6 +60,7 @@
    - [`newAbstract` checker](#newabstract-checker)
    - [`nonPublicInterfaceMember` checker](#nonpublicinterfacemember-checker)
    - [`notExplicitNullableParam` checker (autofixable)](#notexplicitnullableparam-checker)
+   - [`notNullSafety` checker](#notnullsafety-checker)
    - [`offBy1` checker (autofixable)](#offby1-checker)
    - [`oldStyleConstructor` checker](#oldstyleconstructor-checker)
    - [`paramClobber` checker](#paramclobber-checker)
@@ -1199,6 +1200,25 @@ function f(?string $str = null);
 <p><br></p>
 
 
+### `notNullSafety` checker
+
+#### Description
+
+Report not nullsafety call
+
+#### Non-compliant code:
+```php
+function f(A $klass);
+						f(null);
+```
+
+#### Compliant code:
+```php
+reported not safety call
+```
+<p><br></p>
+
+
 ### `offBy1` checker
 
 > Auto fix available
diff --git a/src/linter/block.go b/src/linter/block.go
index fa3a95f..a8dc54d 100644
--- a/src/linter/block.go
+++ b/src/linter/block.go
@@ -1027,7 +1027,202 @@ func (b *blockWalker) handleIssetDimFetch(e *ir.ArrayDimFetchExpr) {
 	}
 }
 
+func nullSafetyRealParamForCheck(fn meta.FuncInfo, paramIndex int, haveVariadic bool) meta.FuncParam {
+	if haveVariadic && paramIndex >= len(fn.Params)-1 {
+		return fn.Params[len(fn.Params)-1]
+	}
+	return fn.Params[paramIndex]
+}
+
+func formatSlashesFuncName(fn meta.FuncInfo) string {
+	return strings.TrimPrefix(fn.Name, "\\")
+}
+
+func (b *blockWalker) checkNullSafetyCallArgsF(args []ir.Node, fn meta.FuncInfo) {
+	if fn.Params == nil || fn.Name == "" {
+		return
+	}
+	haveVariadic := fn.Flags&meta.FuncVariadic != 0
+
+	for i, arg := range args {
+		if arg == nil {
+			continue
+		}
+
+		// If there are more arguments than declared and function is not variadic, ignore extra arguments.
+		if !haveVariadic && i > len(fn.Params)-1 {
+			return
+		}
+
+		switch a := arg.(*ir.Argument).Expr.(type) {
+		case *ir.SimpleVar:
+			b.checkSimpleVarNullSafety(arg, fn, i, a, haveVariadic)
+		case *ir.ConstFetchExpr:
+			b.checkConstFetchNullSafety(arg, fn, i, a, haveVariadic)
+		case *ir.ArrayDimFetchExpr:
+			b.checkArrayDimFetchNullSafety(arg, fn, i, a, haveVariadic)
+		case *ir.ListExpr:
+			b.checkListExprNullSafety(arg, fn, i, a, haveVariadic)
+		case *ir.PropertyFetchExpr:
+			b.checkPropertyFetchNullSafety(a, fn, i, haveVariadic)
+		}
+	}
+}
+
+func (b *blockWalker) checkSimpleVarNullSafety(arg ir.Node, fn meta.FuncInfo, paramIndex int, variable *ir.SimpleVar, haveVariadic bool) {
+	varInfo, ok := b.ctx.sc.GetVar(variable)
+	if !ok {
+		return
+	}
+
+	param := nullSafetyRealParamForCheck(fn, paramIndex, haveVariadic)
+	if haveVariadic && paramIndex >= len(fn.Params)-1 {
+		// For variadic parameter check, if type is mixed then skip.
+		if types.IsTypeMixed(param.Typ) {
+			return
+		}
+	}
+	paramAllowsNull := types.IsTypeNullable(param.Typ)
+	varIsNullable := types.IsTypeNullable(varInfo.Type)
+	if varIsNullable && !paramAllowsNull {
+		b.report(arg, LevelWarning, "notNullSafety",
+			"not null safety call in function %s signature of param %s",
+			formatSlashesFuncName(fn), param.Name)
+	}
+}
+
+func (b *blockWalker) checkConstFetchNullSafety(arg ir.Node, fn meta.FuncInfo, paramIndex int, constExpr *ir.ConstFetchExpr, haveVariadic bool) {
+	constVal := constExpr.Constant.Value
+	isNull := constVal == "null"
+
+	param := nullSafetyRealParamForCheck(fn, paramIndex, haveVariadic)
+	if haveVariadic && paramIndex >= len(fn.Params)-1 {
+		if types.IsTypeMixed(param.Typ) {
+			return
+		}
+	}
+	paramAllowsNull := types.IsTypeNullable(param.Typ)
+	if isNull && !paramAllowsNull {
+		b.report(arg, LevelWarning, "notNullSafety",
+			"null passed to non-nullable parameter %s in function %s",
+			param.Name, formatSlashesFuncName(fn))
+	}
+}
+
+func (b *blockWalker) checkArrayDimFetchNullSafety(arg ir.Node, fn meta.FuncInfo, paramIndex int, arrayExpr *ir.ArrayDimFetchExpr, haveVariadic bool) {
+	baseVar, ok := arrayExpr.Variable.(*ir.SimpleVar)
+	if !ok {
+		return
+	}
+
+	varInfo, found := b.ctx.sc.GetVar(baseVar)
+	if !found {
+		return
+	}
+
+	param := nullSafetyRealParamForCheck(fn, paramIndex, haveVariadic)
+	if haveVariadic && paramIndex >= len(fn.Params)-1 {
+		if types.IsTypeMixed(param.Typ) {
+			return
+		}
+	}
+	paramAllowsNull := types.IsTypeNullable(param.Typ)
+	if types.IsTypeNullable(varInfo.Type) && !paramAllowsNull {
+		b.report(arg, LevelWarning, "notNullSafety",
+			"potential null array access in parameter %s of function %s",
+			param.Name, formatSlashesFuncName(fn))
+	}
+}
+
+func (b *blockWalker) checkListExprNullSafety(arg ir.Node, fn meta.FuncInfo, paramIndex int, listExpr *ir.ListExpr, haveVariadic bool) {
+	for _, item := range listExpr.Items {
+		if item == nil {
+			continue
+		}
+
+		if item.Key != nil {
+			b.checkNullSafetyCallArgsF([]ir.Node{item.Key}, fn)
+		}
+
+		if item.Val != nil {
+			param := nullSafetyRealParamForCheck(fn, paramIndex, haveVariadic)
+			if simpleVar, ok := item.Val.(*ir.SimpleVar); ok {
+				varInfo, found := b.ctx.sc.GetVar(simpleVar)
+				if found && types.IsTypeNullable(varInfo.Type) && !types.IsTypeNullable(param.Typ) {
+					b.report(arg, LevelWarning, "notNullSafety",
+						"potential null value in list assignment for param %s in function %s",
+						param.Name, formatSlashesFuncName(fn))
+				}
+			}
+			b.checkNullSafetyCallArgsF([]ir.Node{item.Val}, fn)
+		}
+	}
+}
+
+func (b *blockWalker) getPropertyComputedType(expr *ir.PropertyFetchExpr) (meta.ClassInfo, types.Map) {
+	baseCall, ok := expr.Variable.(*ir.SimpleVar)
+	if !ok {
+		return meta.ClassInfo{}, types.Map{}
+	}
+
+	varInfo, ok := b.ctx.sc.GetVar(baseCall)
+	if !ok {
+		return meta.ClassInfo{}, types.Map{}
+	}
+
+	classInfo, ok := b.r.ctx.st.Info.GetClass(varInfo.Type.String())
+	if !ok {
+		return meta.ClassInfo{}, types.Map{}
+	}
+
+	property, ok := expr.Property.(*ir.Identifier)
+	if !ok {
+		return meta.ClassInfo{}, types.Map{}
+	}
+
+	propertyInfoFromClass := classInfo.Properties[property.Value]
+	return classInfo, propertyInfoFromClass.Typ
+}
+
+func (b *blockWalker) checkPropertyFetchNullSafety(expr *ir.PropertyFetchExpr, fn meta.FuncInfo, paramIndex int, haveVariadic bool) {
+	// Recursively check the left part of the chain if it is also a property fetch.
+	if nested, ok := expr.Variable.(*ir.PropertyFetchExpr); ok {
+		b.checkPropertyFetchNullSafety(nested, fn, paramIndex, haveVariadic)
+	}
+
+	classInfo, propType := b.getPropertyComputedType(expr)
+	if classInfo.Name == "" || propType.Empty() {
+		return
+	}
+
+	prp, ok := expr.Property.(*ir.Identifier)
+	if !ok {
+		return
+	}
+
+	isPrpNullable := types.IsTypeNullable(propType)
+	param := nullSafetyRealParamForCheck(fn, paramIndex, haveVariadic)
+	if haveVariadic && paramIndex >= len(fn.Params)-1 {
+		if types.IsTypeMixed(param.Typ) {
+			return
+		}
+		paramAllowsNull := types.IsTypeNullable(param.Typ)
+		if isPrpNullable && !paramAllowsNull {
+			b.report(expr, LevelWarning, "notNullSafety",
+				"potential null dereference when accessing property '%s'", prp.Value)
+		}
+		return
+	}
+
+	paramAllowsNull := types.IsTypeNullable(param.Typ)
+	if isPrpNullable && !paramAllowsNull {
+		b.report(expr, LevelWarning, "notNullSafety",
+			"potential null dereference when accessing property '%s'", prp.Value)
+	}
+}
 func (b *blockWalker) handleCallArgs(args []ir.Node, fn meta.FuncInfo) {
+	b.checkNullSafetyCallArgsF(args, fn)
+
 	for i, arg := range args {
 		if i >= len(fn.Params) {
 			arg.Walk(b)
@@ -1388,13 +1583,19 @@ func (b *blockWalker) enterClosure(fun *ir.ClosureExpr, haveThis bool, thisType
 		b.r.meta.Functions = meta.NewFunctionsMap()
 	}
 
+	var funcFlags meta.FuncFlags
+
+	if params.isVariadic {
+		funcFlags |= meta.FuncVariadic
+	}
+
 	b.r.meta.Functions.Set(name, meta.FuncInfo{
 		Params:          params.params,
 		Name:            name,
 		Pos:             b.r.getElementPos(fun),
 		Typ:             returnTypes.Immutable(),
 		MinParamsCnt:    params.minParamsCount,
-		Flags:           0,
+		Flags:           funcFlags,
 		ExitFlags:       exitFlags,
 		DeprecationInfo: doc.Deprecation,
 	})
diff --git a/src/linter/cache.go b/src/linter/cache.go
index b18c7a5..bf66e43 100644
--- a/src/linter/cache.go
+++ b/src/linter/cache.go
@@ -17,42 +17,43 @@ import (
 //
 // Version log:
 //
-//		27 - added Static field to meta.FuncInfo
-//		28 - array type parsed as mixed[]
-//		29 - updated type inference for ClassConstFetch
-//		30 - resolve ClassConstFetch to a wrapped type string
-//		31 - fixed plus operator type inference for arrays
-//		32 - replaced Static:bool with Flags:uint8 in meta.FuncInfo
-//		33 - support parsing of array<k,v> and list<type>
-//		34 - support parsing of ?ClassName as "ClassName|null"
-//		35 - added Flags:uint8 to meta.ClassInfo
-//		36 - added FuncAbstract bit to FuncFlags
-//		     added FuncFinal bit to FuncFlags
-//		     added ClassFinal bit to ClassFlags
-//		     FuncInfo now stores original function name
-//		     ClassInfo now stores original class name
-//		37 - added ClassShape bit to ClassFlags
-//		     changed meta.scopeVar bool fields representation
-//		38 - replaced TypesMap.immutable:bool with flags:uint8.
-//		     added mapPrecise flag to mark precise type maps.
-//		39 - added new field Value in ConstantInfo
-//		40 - changed string const value storage (no quotes)
-//		41 - const-folding affected const definition values
-//		42 - bool-typed consts are now stored in meta info
-//		43 - define'd const values stored in cache
-//		44 - rename ConstantInfo => ConstInfo
-//		45 - added Mixins field to meta.ClassInfo
-//		46 - changed the way of inferring the return type of functions and methods
-//		47 - forced cache version invalidation due to the #921
-//		48 - renamed meta.TypesMap to types.Map; this affects gob encoding
-//		49 - for shape, names are now generated using the keys that make up this shape
-//		50 - added Flags field for meta.PropertyInfo
-//		51 - added anonymous classes
-//		52 - renamed all PhpDoc and Phpdoc with PHPDoc
-//		53 - added DeprecationInfo for functions and methods and support for some attributes
-//		54 - forced cache version invalidation due to the #1165
-//	 55 - updated go version 1.16 -> 1.21
-const cacheVersion = 55
+//			27 - added Static field to meta.FuncInfo
+//			28 - array type parsed as mixed[]
+//			29 - updated type inference for ClassConstFetch
+//			30 - resolve ClassConstFetch to a wrapped type string
+//			31 - fixed plus operator type inference for arrays
+//			32 - replaced Static:bool with Flags:uint8 in meta.FuncInfo
+//			33 - support parsing of array<k,v> and list<type>
+//			34 - support parsing of ?ClassName as "ClassName|null"
+//			35 - added Flags:uint8 to meta.ClassInfo
+//			36 - added FuncAbstract bit to FuncFlags
+//			     added FuncFinal bit to FuncFlags
+//			     added ClassFinal bit to ClassFlags
+//			     FuncInfo now stores original function name
+//			     ClassInfo now stores original class name
+//			37 - added ClassShape bit to ClassFlags
+//			     changed meta.scopeVar bool fields representation
+//			38 - replaced TypesMap.immutable:bool with flags:uint8.
+//			     added mapPrecise flag to mark precise type maps.
+//			39 - added new field Value in ConstantInfo
+//			40 - changed string const value storage (no quotes)
+//			41 - const-folding affected const definition values
+//			42 - bool-typed consts are now stored in meta info
+//			43 - define'd const values stored in cache
+//			44 - rename ConstantInfo => ConstInfo
+//			45 - added Mixins field to meta.ClassInfo
+//			46 - changed the way of inferring the return type of functions and methods
+//			47 - forced cache version invalidation due to the #921
+//			48 - renamed meta.TypesMap to types.Map; this affects gob encoding
+//			49 - for shape, names are now generated using the keys that make up this shape
+//			50 - added Flags field for meta.PropertyInfo
+//			51 - added anonymous classes
+//			52 - renamed all PhpDoc and Phpdoc with PHPDoc
+//			53 - added DeprecationInfo for functions and methods and support for some attributes
+//			54 - forced cache version invalidation due to the #1165
+//		 	55 - updated go version 1.16 -> 1.21
+//	     56 - added isVariadic to meta.FuncInfo
+const cacheVersion = 56
 
 var (
 	errWrongVersion = errors.New("Wrong cache version")
diff --git a/src/linter/report.go b/src/linter/report.go
index 9ef7e1f..26c247f 100644
--- a/src/linter/report.go
+++ b/src/linter/report.go
@@ -25,6 +25,16 @@ func addBuiltinCheckers(reg *CheckersRegistry) {
 			After:    `$s = strip_tags($s, '<br>')`,
 		},
 
+		{
+			Name:     "notNullSafety",
+			Default:  true,
+			Quickfix: false,
+			Comment:  "Report not nullsafety call",
+			Before: `function f(A $klass);
+						f(null);`,
+			After: `reported not safety call`,
+		},
+
 		{
 			Name:     "notExplicitNullableParam",
 			Default:  true,
diff --git a/src/linter/root.go b/src/linter/root.go
index b7fe6fa..03cdf2a 100644
--- a/src/linter/root.go
+++ b/src/linter/root.go
@@ -486,6 +486,7 @@ type parseFuncParamsResult struct {
 	params         []meta.FuncParam
 	paramsTypeHint map[string]types.Map
 	minParamsCount int
+	isVariadic     bool
 }
 
 func (d *rootWalker) parseFuncParams(params []ir.Node, docblockParams phpdoctypes.ParamsMap, sc *meta.Scope, closureSolver *solver.ClosureCallerInfo) (res parseFuncParamsResult) {
@@ -496,6 +497,7 @@ func (d *rootWalker) parseFuncParams(params []ir.Node, docblockParams phpdoctype
 	minArgs := 0
 	parsedParams := make([]meta.FuncParam, 0, len(params))
 	paramHints := make(map[string]types.Map, len(params))
+	isVariadic := false
 
 	if closureSolver != nil && solver.IsClosureUseFunction(closureSolver.Name) {
 		return d.parseFuncArgsForCallback(params, sc, closureSolver)
@@ -535,6 +537,8 @@ func (d *rootWalker) parseFuncParams(params []ir.Node, docblockParams phpdoctype
 		// Handle variadic.
 		if param.Variadic {
 			paramType = paramType.Map(types.WrapArrayOf)
+
+			isVariadic = true
 		}
 
 		// Append @param type.
@@ -565,6 +569,7 @@ func (d *rootWalker) parseFuncParams(params []ir.Node, docblockParams phpdoctype
 		params:         parsedParams,
 		paramsTypeHint: paramHints,
 		minParamsCount: minArgs,
+		isVariadic:     isVariadic,
 	}
 }
 
@@ -664,6 +669,10 @@ func (d *rootWalker) enterFunction(fun *ir.FunctionStmt) bool {
 	if solver.SideEffectFreeFunc(d.scope(), d.ctx.st, nil, fun.Stmts) {
 		funcFlags |= meta.FuncPure
 	}
+	if funcParams.isVariadic {
+		funcFlags |= meta.FuncVariadic
+	}
+
 	d.meta.Functions.Set(nm, meta.FuncInfo{
 		Params:          funcParams.params,
 		Name:            nm,
@@ -1135,6 +1144,10 @@ func (d *rootWalker) enterClassMethod(meth *ir.ClassMethodStmt) bool {
 	if !insideInterface && !modif.abstract && solver.SideEffectFreeFunc(d.scope(), d.ctx.st, nil, stmts) {
 		funcFlags |= meta.FuncPure
 	}
+	if funcParams.isVariadic {
+		funcFlags |= meta.FuncVariadic
+	}
+
 	class.Methods.Set(nm, meta.FuncInfo{
 		Params:          funcParams.params,
 		Name:            nm,
diff --git a/src/meta/metainfo.go b/src/meta/metainfo.go
index 160d7a5..38f88c0 100644
--- a/src/meta/metainfo.go
+++ b/src/meta/metainfo.go
@@ -15,6 +15,7 @@ const (
 	FuncFinal
 	// FuncFromAnnotation is set if the function is described in the class annotation.
 	FuncFromAnnotation
+	FuncVariadic
 )
 
 type PropertyFlags uint8
diff --git a/src/types/predicates.go b/src/types/predicates.go
index d0b4034..3d963e3 100644
--- a/src/types/predicates.go
+++ b/src/types/predicates.go
@@ -37,6 +37,23 @@ func IsAlias(s string) bool {
 	return has
 }
 
+func IsTypeNullable(typ Map) bool {
+	isNullable := false
+	if typ.m == nil {
+		return true // We consider that if the type is not inferred, then it is mixed
+	}
+	typ.Iterate(func(t string) {
+		if strings.Contains(t, "null") || strings.Contains(t, "mixed") {
+			isNullable = true
+		}
+	})
+	return isNullable
+}
+
+func IsTypeMixed(typ Map) bool {
+	return typ.Contains("mixed")
+}
+
 func Alias(s string) (string, bool) {
 	alias, has := aliases[s]
 	return alias, has


Test patch:
diff --git a/src/tests/checkers/null_safety_test.go b/src/tests/checkers/null_safety_test.go
new file mode 100644
index 0000000..bb8ac88
--- /dev/null
+++ b/src/tests/checkers/null_safety_test.go
@@ -0,0 +1,243 @@
+package checkers
+
+import (
+	"testing"
+
+	"github.com/VKCOM/noverify/src/linttest"
+)
+
+func TestFunctionPassingNull(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+class A {
+    public string $value = 'Hello';
+}
+
+function test(A $a): void {
+    echo $a->value;
+}
+
+test(null);
+`)
+	test.Expect = []string{
+		"null passed to non-nullable parameter a in function test",
+	}
+	test.RunAndMatch()
+}
+
+func TestPropertyFetchNullSafety(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+class A {
+    public ?B $b = null;
+}
+class B {
+    public string $value = 'Test';
+}
+
+function test(B $b): void {
+    echo $b->value;
+}
+
+$a = new A();
+test($a->b);
+`)
+	test.Expect = []string{
+		"potential null dereference when accessing property 'b'",
+	}
+	test.RunAndMatch()
+}
+
+func TestChainedPropertyAccessNullSafety(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+declare(strict_types=1);
+
+class A {
+    public ?B $b = null;
+}
+
+class B {
+    public C $c;
+    public function __construct() {
+        // Initialize property c with an instance of C.
+        $this->c = new C();
+    }
+}
+
+class C {
+    public string $value = 'Hello';
+}
+
+/**
+ * Function expecting an object of class C.
+ */
+function test(C $c): void {
+    echo $c->value;
+}
+
+$a = new A();
+// $a->b is null, so accessing $a->b->c leads to a potential null dereference.
+test($a->b->c);
+`)
+	test.Expect = []string{
+		"potential null dereference when accessing property",
+	}
+	test.RunAndMatch()
+}
+
+// After fix issue with not correct checking existing variable after unpacking - should fail
+func TestListExprNullSafety(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+class A {
+    public string $value = 'Hello';
+}
+
+function test(A $a): void {
+    echo $a->value;
+}
+
+list($m, list($n, $o)) = [new A(), [new A(), null]];
+
+test($m);
+test($n); 
+test($o);
+`)
+	test.Expect = []string{
+		"not null safety call in function test signature of param",
+		"Cannot find referenced variable $o",
+		"Cannot find referenced variable $n",
+	}
+	test.RunAndMatch()
+}
+
+func TestArrayDimFetchNullSafety(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+class A {
+    public string $value = 'Hello';
+}
+
+function test(A $a): void {
+    echo $a->value;
+}
+
+$arr = [new A(), null];
+test($arr[1]);
+`)
+	test.Expect = []string{
+		"potential null array access in parameter a of function test",
+	}
+	test.RunAndMatch()
+}
+
+func TestVariadicParameterNullSafety(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+class A {
+    public string $value = 'Hello';
+}
+
+/**
+ * Variadic function accepting only objects of class A.
+ */
+function testVariadic(A ...$a): void {
+    foreach ($a as $item) {
+        echo $item->value, "\n";
+    }
+}
+
+testVariadic(new A(), null);
+`)
+	test.Expect = []string{
+		"null passed to non-nullable parameter a in function testVariadic",
+	}
+	test.RunAndMatch()
+}
+
+// TODO: After realisation Control Flow Graph (CFG) и Data Flow Graph (DFG) this test must fail
+func TestIfNullCheckSafe(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+declare(strict_types=1);
+
+class A {
+    public string $value = 'Safe';
+}
+
+/**
+ * Function expecting a non-null instance of A.
+ */
+function test(A $a): void {
+    echo $a->value;
+}
+
+$v = null;
+if ($v == null) {
+    // Correctly assign a new instance when $v is null.
+    $v = new A();
+}
+test($v); // Should be safe.
+`)
+
+	test.Expect = []string{
+		`not null safety call in function test signature of param`,
+	}
+	test.RunAndMatch()
+}
+
+func TestIfNullCheckUnsafe(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+declare(strict_types=1);
+
+class A {
+    public string $value = 'Unsafe';
+}
+
+/**
+ * Function expecting a non-null instance of A.
+ */
+function test(A $a): void {
+    echo $a->value;
+}
+
+$v = null;
+if ($v == null) {
+    // No assignment is done here – $v remains null.
+}
+test($v); // Should trigger a null safety error.
+`)
+	test.Expect = []string{
+		"not null safety call in function test signature of param",
+	}
+	test.RunAndMatch()
+}
+
+func TestIfNotNullCheck(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+declare(strict_types=1);
+
+class A {
+    public string $value = 'NotNull';
+}
+
+/**
+ * Function expecting a non-null instance of A.
+ */
+function test(A $a): void {
+    echo $a->value;
+}
+
+$v = new A();
+if ($v !== null) {
+    // $v is known to be non-null.
+    test($v); // Should be safe.
+}
+`)
+
+	test.Expect = []string{}
+	test.RunAndMatch()
+}
diff --git a/src/tests/checkers/regexpsimplify_test.go b/src/tests/checkers/regexpsimplify_test.go
index c501a0d..d7a2645 100644
--- a/src/tests/checkers/regexpsimplify_test.go
+++ b/src/tests/checkers/regexpsimplify_test.go
@@ -19,6 +19,9 @@ function f($s) {
 	test.Expect = []string{
 		`May re-write '~(?P<abc>[0-9])~' as '~(?P<abc>\d)~'`,
 		`May re-write '~(?<abc>[0-9])~' as '~(?<abc>\d)~'`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
 	}
 	test.RunAndMatch()
 }
@@ -39,6 +42,9 @@ function f($s) {
 		`May re-write '/^write([-]?\d\d*)?$/i' as '/^write(-?\d+)?$/i'`,
 		`May re-write '/[a-a]/' as '/a/'`,
 		`May re-write '/[a-a]*?/' as '/a*?/'`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match_all signature of param subject`,
+		`not null safety call in function preg_split signature of param subject`,
 	}
 	test.RunAndMatch()
 }
@@ -113,6 +119,7 @@ function unrepeat($s) {
   preg_match('/x{0,}/', $s);
 }
 `)
+
 	test.Expect = []string{
 		`May re-write '/(?:x)/' as '/x/'`,
 		`May re-write '/(?:[abc])/' as '/[abc]/'`,
@@ -138,6 +145,30 @@ function unrepeat($s) {
 		`May re-write '/[0-9]{1,}/' as '/\d+/'`,
 		`May re-write '/[0-9]{0,1}/' as '/\d?/'`,
 		`May re-write '/x{0,}/' as '/x*/'`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
 	}
 	test.RunAndMatch()
 }
@@ -158,6 +189,10 @@ function f($s) {
 		`May re-write '/^http:\/\/~/' as '@^http://~@'`,
 		`May re-write '/^http:\/\/~@/' as '#^http://~@#'`,
 		`May re-write '@mail\@gmail\.ru@' as '/mail@gmail\.ru/'`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
 	}
 	test.RunAndMatch()
 }
@@ -354,5 +389,173 @@ function _6($s) {
   preg_match('/(?i)\([^)]*mix[^)]*\)$/', $s);
 }
 `)
+	test.Expect = []string{
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+	}
 	test.RunAndMatch()
 }
diff --git a/src/tests/checkers/regexpvet_test.go b/src/tests/checkers/regexpvet_test.go
index c97770d..66b2622 100644
--- a/src/tests/checkers/regexpvet_test.go
+++ b/src/tests/checkers/regexpvet_test.go
@@ -26,6 +26,10 @@ function parseErrors($s) {
 		`parse error: 'a' is not a valid delimiter`,
 		`parse error: can't find '>' ending delimiter`,
 		`parse error: unterminated '['`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
 	}
 	test.RunAndMatch()
 }
@@ -204,6 +208,9 @@ function f($s) {
 		`'\s' intersects with '\t' in [\s\t]`,
 		`'e' is duplicated in [com|org|edu|net]`,
 		`'\w' intersects with '\d' in [\w\d]`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
 	}
 	test.RunAndMatch()
 }
@@ -273,5 +280,31 @@ function goodAnchors($s) {
 	preg_match('~(?i)(?:)(^| )\S+~', $s);
 }
 `)
+	test.Expect = []string{
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+		`not null safety call in function preg_match signature of param subject`,
+	}
 	test.RunAndMatch()
 }
diff --git a/src/tests/checkers/trigger_error_test.go b/src/tests/checkers/trigger_error_test.go
index a5d909a..82432fa 100644
--- a/src/tests/checkers/trigger_error_test.go
+++ b/src/tests/checkers/trigger_error_test.go
@@ -50,6 +50,7 @@ echo 'unreachable';
 `)
 	test.Expect = []string{
 		`Unreachable code`,
+		`not null safety call in function trigger_error signature of param message`,
 	}
 	test.RunAndMatch()
 }
diff --git a/src/tests/golden/golden_test.go b/src/tests/golden/golden_test.go
index 88657c9..19dddd6 100644
--- a/src/tests/golden/golden_test.go
+++ b/src/tests/golden/golden_test.go
@@ -96,7 +96,7 @@ func TestGolden(t *testing.T) {
 
 		{
 			Name:    "parsedown",
-			Disable: []string{"missingPhpdoc", "arraySyntax", "phpAliases"},
+			Disable: []string{"missingPhpdoc", "arraySyntax", "phpAliases", "notNullSafety"},
 			Deps: []string{
 				`stubs/phpstorm-stubs/pcre/pcre.php`,
 				`stubs/phpstorm-stubs/mbstring/mbstring.php`,
@@ -105,7 +105,7 @@ func TestGolden(t *testing.T) {
 
 		{
 			Name:    "underscore",
-			Disable: []string{"missingPhpdoc", "phpAliases"},
+			Disable: []string{"missingPhpdoc", "phpAliases", "notNullSafety"},
 			Deps: []string{
 				`stubs/phpstorm-stubs/pcre/pcre.php`,
 			},
@@ -113,7 +113,7 @@ func TestGolden(t *testing.T) {
 
 		{
 			Name:    "phprocksyd",
-			Disable: []string{"missingPhpdoc"},
+			Disable: []string{"missingPhpdoc", "notNullSafety"},
 			Deps: []string{
 				`stubs/phpstorm-stubs/standard/basic.php`,
 				`stubs/phpstorm-stubs/pcntl/pcntl.php`,
@@ -124,7 +124,7 @@ func TestGolden(t *testing.T) {
 
 		{
 			Name:    "flysystem",
-			Disable: []string{"redundantCast"},
+			Disable: []string{"redundantCast", "notNullSafety"},
 			Deps: []string{
 				`stubs/phpstorm-stubs/pcre/pcre.php`,
 				`stubs/phpstorm-stubs/SPL/SPL.php`,
@@ -141,7 +141,7 @@ func TestGolden(t *testing.T) {
 
 		{
 			Name:    "inflector",
-			Disable: []string{"missingPhpdoc"},
+			Disable: []string{"missingPhpdoc", "notNullSafety"},
 			Deps: []string{
 				`stubs/phpstorm-stubs/pcre/pcre.php`,
 				`stubs/phpstorm-stubs/SPL/SPL.php`,
@@ -151,7 +151,7 @@ func TestGolden(t *testing.T) {
 
 		{
 			Name:    "options-resolver",
-			Disable: []string{"missingPhpdoc"},
+			Disable: []string{"missingPhpdoc", "notNullSafety"},
 			Deps: []string{
 				`stubs/phpstorm-stubs/SPL/SPL.php`,
 				`stubs/phpstorm-stubs/Reflection/Reflection.php`,
@@ -164,7 +164,7 @@ func TestGolden(t *testing.T) {
 
 		{
 			Name:    "twitter-api-php",
-			Disable: []string{"missingPhpdoc", "arraySyntax"},
+			Disable: []string{"missingPhpdoc", "arraySyntax", "notNullSafety"},
 			Deps: []string{
 				`stubs/phpstorm-stubs/pcre/pcre.php`,
 				`stubs/phpstorm-stubs/SPL/SPL.php`,
diff --git a/src/tests/golden/testdata/embeddedrules/golden.txt b/src/tests/golden/testdata/embeddedrules/golden.txt
index e041874..50f964a 100644
--- a/src/tests/golden/testdata/embeddedrules/golden.txt
+++ b/src/tests/golden/testdata/embeddedrules/golden.txt
@@ -13,9 +13,21 @@ WARNING argsOrder: Potentially incorrect haystack and needle arguments order at
 WARNING argsOrder: Potentially incorrect replacement and subject arguments order at testdata/embeddedrules/argsOrder.php:32
   $_ = preg_replace($pat, $subj, 'replacement');
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function explode signature of param separator at testdata/embeddedrules/argsOrder.php:41
+  $_ = explode($s, '/');
+               ^^
 WARNING argsOrder: Potentially incorrect delimiter and string arguments order at testdata/embeddedrules/argsOrder.php:41
   $_ = explode($s, '/');
        ^^^^^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function explode signature of param string at testdata/embeddedrules/argsOrder.php:45
+  $_ = explode('/', $s);
+                    ^^
+WARNING notNullSafety: not null safety call in function explode signature of param separator at testdata/embeddedrules/argsOrder.php:46
+  $_ = explode($delim, $s);
+               ^^^^^^
+WARNING notNullSafety: not null safety call in function explode signature of param string at testdata/embeddedrules/argsOrder.php:46
+  $_ = explode($delim, $s);
+                       ^^
 WARNING argsOrder: Potentially incorrect replace and string arguments order at testdata/embeddedrules/argsOrder.php:50
   $_ = str_replace($search, $subj, ' ');
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/src/tests/golden/testdata/idn/golden.txt b/src/tests/golden/testdata/idn/golden.txt
index 17aace3..564ebef 100644
--- a/src/tests/golden/testdata/idn/golden.txt
+++ b/src/tests/golden/testdata/idn/golden.txt
@@ -7,6 +7,9 @@ MAYBE   arraySyntax: Use the short form '[]' instead of the old 'array()' at tes
 WARNING errorSilence: Don't use @, silencing errors is bad practice at testdata/idn/idn.php:67
             @trigger_error('idn_to_ascii(): INTL_IDNA_VARIANT_2003 is deprecated', E_USER_DEPRECATED);
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function mb_strtolower signature of param string at testdata/idn/idn.php:71
+            $domain = mb_strtolower($domain, 'utf-8');
+                                    ^^^^^^^
 MAYBE   arraySyntax: Use the short form '[]' instead of the old 'array()' at testdata/idn/idn.php:87
         $idna_info = array(
                      
@@ -19,9 +22,15 @@ MAYBE   arraySyntax: Use the short form '[]' instead of the old 'array()' at tes
 WARNING errorSilence: Don't use @, silencing errors is bad practice at testdata/idn/idn.php:99
             @trigger_error('idn_to_utf8(): INTL_IDNA_VARIANT_2003 is deprecated', E_USER_DEPRECATED);
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function explode signature of param string at testdata/idn/idn.php:102
+        $parts = explode('.', $domain);
+                              ^^^^^^^
 MAYBE   arraySyntax: Use the short form '[]' instead of the old 'array()' at testdata/idn/idn.php:119
         $idna_info = array(
                      
+WARNING notNullSafety: not null safety call in function mb_strlen signature of param string at testdata/idn/idn.php:152
+        $length = mb_strlen($input, 'utf-8');
+                            ^^^^^^
 MAYBE   arraySyntax: Use the short form '[]' instead of the old 'array()' at testdata/idn/idn.php:195
             'all' => array(),
                      ^^^^^^^
@@ -34,6 +43,12 @@ MAYBE   arraySyntax: Use the short form '[]' instead of the old 'array()' at tes
 MAYBE   arraySyntax: Use the short form '[]' instead of the old 'array()' at testdata/idn/idn.php:194
         $codePoints = array(
                       
+WARNING notNullSafety: not null safety call in function mb_strlen signature of param string at testdata/idn/idn.php:200
+        $length = mb_strlen($input, 'utf-8');
+                            ^^^^^^
+WARNING notNullSafety: not null safety call in function mb_substr signature of param string at testdata/idn/idn.php:202
+            $char = mb_substr($input, $i, 1, 'utf-8');
+                              ^^^^^^
 MAYBE   redundantCast: Expression already has int type at testdata/idn/idn.php:233
             $delta = (int) ($delta / 35);
                            ^^^^^^^^^^^^^
@@ -43,6 +58,15 @@ MAYBE   assignOp: Could rewrite as `$k += 36` at testdata/idn/idn.php:234
 MAYBE   redundantCast: Expression already has int type at testdata/idn/idn.php:237
         return $k + (int) (36 * $delta / ($delta + 38));
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function strrpos signature of param haystack at testdata/idn/idn.php:247
+        $pos = strrpos($input, '-');
+                       ^^^^^^
+WARNING notNullSafety: not null safety call in function substr signature of param string at testdata/idn/idn.php:249
+            $output = substr($input, 0, $pos++);
+                             ^^^^^^
+WARNING notNullSafety: not null safety call in function strlen signature of param string at testdata/idn/idn.php:255
+        $inputLength = \strlen($input);
+                               ^^^^^^
 MAYBE   assignOp: Could rewrite as `$n += (int) ($i / $outputLength)` at testdata/idn/idn.php:274
             $n = $n + (int) ($i / $outputLength);
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/src/tests/golden/testdata/math/golden.txt b/src/tests/golden/testdata/math/golden.txt
index ae3f8b9..80fa121 100644
--- a/src/tests/golden/testdata/math/golden.txt
+++ b/src/tests/golden/testdata/math/golden.txt
@@ -1,18 +1,42 @@
 WARNING unused: Variable $a is unused (use $_ to ignore this inspection or specify --unused-var-regex flag) at testdata/math/src/BigDecimal.php:292
         [$a, $b] = $this->scaleValues($this, $that);
          ^^
+WARNING notNullSafety: not null safety call in function of signature of param value at testdata/math/src/BigDecimal.php:588
+        $that = BigNumber::of($that);
+                              ^^^^^
 MAYBE   trailingComma: Last element in a multi-line array should have a trailing comma at testdata/math/src/BigInteger.php:435
             new BigInteger($remainder)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function of signature of param value at testdata/math/src/BigInteger.php:742
+        $that = BigNumber::of($that);
+                              ^^^^^
 MAYBE   ternarySimplify: Could rewrite as `$matches['fractional'] ?? ''` at testdata/math/src/BigNumber.php:90
             $fractional = isset($matches['fractional']) ? $matches['fractional'] : '';
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function isLessThan signature of param that at testdata/math/src/BigNumber.php:173
+            if ($min === null || $value->isLessThan($min)) {
+                                                    ^^^^
+WARNING notNullSafety: not null safety call in function isGreaterThan signature of param that at testdata/math/src/BigNumber.php:205
+            if ($max === null || $value->isGreaterThan($max)) {
+                                                       ^^^^
+WARNING notNullSafety: not null safety call in function add signature of param a at testdata/math/src/BigNumber.php:241
+                $sum = self::add($sum, $value);
+                                 ^^^^
+WARNING notNullSafety: not null safety call in function minus signature of param that at testdata/math/src/BigRational.php:365
+        return $this->minus($that)->getSign();
+                            ^^^^^
 MAYBE   implicitModifiers: Specify the access modifier for \Brick\Math\Internal\Calculator::powmod method explicitly at testdata/math/src/Internal/Calculator.php:260
     abstract function powmod(string $base, string $exp, string $mod) : string;
                       ^^^^^^
 MAYBE   assignOp: Could rewrite as `$number ^= $xor` at testdata/math/src/Internal/Calculator.php:622
         $number = $number ^ $xor;
         ^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function gmp_strval signature of param num at testdata/math/src/Internal/Calculator/GmpCalculator.php:66
+            \gmp_strval($q),
+                        ^^
+WARNING notNullSafety: not null safety call in function gmp_strval signature of param num at testdata/math/src/Internal/Calculator/GmpCalculator.php:67
+            \gmp_strval($r)
+                        ^^
 MAYBE   trailingComma: Last element in a multi-line array should have a trailing comma at testdata/math/src/Internal/Calculator/GmpCalculator.php:67
             \gmp_strval($r)
             ^^^^^^^^^^^^^^^
diff --git a/src/tests/golden/testdata/mustache/golden.txt b/src/tests/golden/testdata/mustache/golden.txt
index c5667a9..9987a66 100644
--- a/src/tests/golden/testdata/mustache/golden.txt
+++ b/src/tests/golden/testdata/mustache/golden.txt
@@ -1,3 +1,6 @@
+WARNING notNullSafety: not null safety call in function realpath signature of param path at testdata/mustache/src/Mustache/Autoloader.php:39
+        $realDir = realpath($baseDir);
+                            ^^^^^^^^
 MAYBE   ternarySimplify: Could rewrite as `$baseDir ?: 0` at testdata/mustache/src/Mustache/Autoloader.php:56
         $key = $baseDir ? $baseDir : 0;
                ^^^^^^^^^^^^^^^^^^^^^^^
@@ -25,24 +28,87 @@ WARNING errorSilence: Don't use @, silencing errors is bad practice at testdata/
 WARNING useEval: Don't use the 'eval' function at testdata/mustache/src/Mustache/Cache/NoopCache.php:45
         eval('?>' . $value);
         ^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter id of function section at testdata/mustache/src/Mustache/Compiler.php:98
+                        $node[Mustache_Tokenizer::NAME],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter start of function section at testdata/mustache/src/Mustache/Compiler.php:100
+                        $node[Mustache_Tokenizer::INDEX],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter end of function section at testdata/mustache/src/Mustache/Compiler.php:101
+                        $node[Mustache_Tokenizer::END],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter otag of function section at testdata/mustache/src/Mustache/Compiler.php:102
+                        $node[Mustache_Tokenizer::OTAG],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter ctag of function section at testdata/mustache/src/Mustache/Compiler.php:103
+                        $node[Mustache_Tokenizer::CTAG],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 MAYBE   ternarySimplify: Could rewrite as `$node[Mustache_Tokenizer::FILTERS] ?? array()` at testdata/mustache/src/Mustache/Compiler.php:99
                         isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter id of function invertedSection at testdata/mustache/src/Mustache/Compiler.php:111
+                        $node[Mustache_Tokenizer::NAME],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 MAYBE   ternarySimplify: Could rewrite as `$node[Mustache_Tokenizer::FILTERS] ?? array()` at testdata/mustache/src/Mustache/Compiler.php:112
                         isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter id of function partial at testdata/mustache/src/Mustache/Compiler.php:119
+                        $node[Mustache_Tokenizer::NAME],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 MAYBE   ternarySimplify: Could rewrite as `$node[Mustache_Tokenizer::INDENT] ?? ''` at testdata/mustache/src/Mustache/Compiler.php:120
                         isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter id of function parent at testdata/mustache/src/Mustache/Compiler.php:127
+                        $node[Mustache_Tokenizer::NAME],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 MAYBE   ternarySimplify: Could rewrite as `$node[Mustache_Tokenizer::INDENT] ?? ''` at testdata/mustache/src/Mustache/Compiler.php:128
                         isset($node[Mustache_Tokenizer::INDENT]) ? $node[Mustache_Tokenizer::INDENT] : '',
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter id of function blockArg at testdata/mustache/src/Mustache/Compiler.php:137
+                        $node[Mustache_Tokenizer::NAME],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter start of function blockArg at testdata/mustache/src/Mustache/Compiler.php:138
+                        $node[Mustache_Tokenizer::INDEX],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter end of function blockArg at testdata/mustache/src/Mustache/Compiler.php:139
+                        $node[Mustache_Tokenizer::END],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter otag of function blockArg at testdata/mustache/src/Mustache/Compiler.php:140
+                        $node[Mustache_Tokenizer::OTAG],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter ctag of function blockArg at testdata/mustache/src/Mustache/Compiler.php:141
+                        $node[Mustache_Tokenizer::CTAG],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter id of function blockVar at testdata/mustache/src/Mustache/Compiler.php:149
+                        $node[Mustache_Tokenizer::NAME],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter start of function blockVar at testdata/mustache/src/Mustache/Compiler.php:150
+                        $node[Mustache_Tokenizer::INDEX],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter end of function blockVar at testdata/mustache/src/Mustache/Compiler.php:151
+                        $node[Mustache_Tokenizer::END],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter otag of function blockVar at testdata/mustache/src/Mustache/Compiler.php:152
+                        $node[Mustache_Tokenizer::OTAG],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter ctag of function blockVar at testdata/mustache/src/Mustache/Compiler.php:153
+                        $node[Mustache_Tokenizer::CTAG],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter id of function variable at testdata/mustache/src/Mustache/Compiler.php:165
+                        $node[Mustache_Tokenizer::NAME],
+                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 MAYBE   ternarySimplify: Could rewrite as `$node[Mustache_Tokenizer::FILTERS] ?? array()` at testdata/mustache/src/Mustache/Compiler.php:166
                         isset($node[Mustache_Tokenizer::FILTERS]) ? $node[Mustache_Tokenizer::FILTERS] : array(),
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter text of function text at testdata/mustache/src/Mustache/Compiler.php:173
+                    $code .= $this->text($node[Mustache_Tokenizer::VALUE], $level);
+                                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 WARNING unused: Variable $keystr is unused (use $_ to ignore this inspection or specify --unused-var-regex flag) at testdata/mustache/src/Mustache/Compiler.php:289
         $keystr = var_export($key, true);
         ^^^^^^^
+WARNING notNullSafety: not null safety call in function getFindMethod signature of param id at testdata/mustache/src/Mustache/Compiler.php:556
+        $method   = $this->getFindMethod($name);
+                                         ^^^^^
 MAYBE   callSimplify: Could simplify to $id[0] at testdata/mustache/src/Mustache/Compiler.php:646
             if (substr($id, 0, 1) === '.') {
                 ^^^^^^^^^^^^^^^^^
@@ -121,12 +187,33 @@ MAYBE   callSimplify: Could simplify to $this->stack[] = $value at testdata/must
 MAYBE   callSimplify: Could simplify to $this->blockStack[] = $value at testdata/mustache/src/Mustache/Context.php:49
         array_push($this->blockStack, $value);
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function findVariableInStack signature of param id at testdata/mustache/src/Mustache/Context.php:131
+        $value  = $this->findVariableInStack($first, $this->stack);
+                                             ^^^^^^
+WARNING notNullSafety: not null safety call in function findVariableInStack signature of param id at testdata/mustache/src/Mustache/Context.php:138
+            $value = $this->findVariableInStack($chunk, array($value));
+                                                ^^^^^^
+WARNING notNullSafety: not null safety call in function findVariableInStack signature of param id at testdata/mustache/src/Mustache/Context.php:174
+            $value = $this->findVariableInStack($chunk, array($value));
+                                                ^^^^^^
+WARNING notNullSafety: not null safety call in function method_exists signature of param object_or_class at testdata/mustache/src/Mustache/Context.php:218
+                        if (method_exists($frame, $id)) {
+                                          ^^^^^^
 WARNING switchDefault: Add 'default' branch to avoid unexpected unhandled condition values at testdata/mustache/src/Mustache/Context.php:213
             switch (gettype($frame)) {
             ^
 MAYBE   ternarySimplify: Could rewrite as `$options['cache_file_mode'] ?? null` at testdata/mustache/src/Mustache/Engine.php:156
                 $mode  = isset($options['cache_file_mode']) ? $options['cache_file_mode'] : null;
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function setCache signature of param cache at testdata/mustache/src/Mustache/Engine.php:160
+            $this->setCache($cache);
+                            ^^^^^^
+WARNING notNullSafety: potential null array access in parameter loader of function setLoader at testdata/mustache/src/Mustache/Engine.php:168
+            $this->setLoader($options['loader']);
+                             ^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter partialsLoader of function setPartialsLoader at testdata/mustache/src/Mustache/Engine.php:172
+            $this->setPartialsLoader($options['partials_loader']);
+                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^
 MAYBE   misspellComment: "entitity" is a misspelling of "entity" at testdata/mustache/src/Mustache/Engine.php:254
     public function getEntityFlags()
                     ^^^^^^^^^^^^^^
@@ -151,6 +238,12 @@ ERROR   undefinedMethod: Call to undefined method {\Mustache_Cache}->setLogger()
 MAYBE   ternarySimplify: Could rewrite as `$this->delimiters ?: '{{ }}'` at testdata/mustache/src/Mustache/Engine.php:628
             'delimiters'      => $this->delimiters ? $this->delimiters : '{{ }}',
                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null dereference when accessing property 'charset' at testdata/mustache/src/Mustache/Engine.php:813
+        return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
+                                                                               ^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null dereference when accessing property 'strictCallables' at testdata/mustache/src/Mustache/Engine.php:813
+        return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
+                                                                                               ^^^^^^^^^^^^^^^^^^^^^^
 WARNING notExplicitNullableParam: parameter with null default value should be explicitly nullable at testdata/mustache/src/Mustache/Engine.php:727
     private function loadSource($source, Mustache_Cache $cache = null)
                                          ^^^^^^^^^^^^^^
@@ -175,9 +268,48 @@ MAYBE   missingPhpdoc: Missing PHPDoc for \Mustache_Exception_UnknownTemplateExc
 WARNING notExplicitNullableParam: parameter with null default value should be explicitly nullable at testdata/mustache/src/Mustache/Exception/UnknownTemplateException.php:23
     public function __construct($templateName, Exception $previous = null)
                                                ^^^^^^^^^
+WARNING notNullSafety: not null safety call in function addLoader signature of param loader at testdata/mustache/src/Mustache/Loader/CascadingLoader.php:35
+            $this->addLoader($loader);
+                             ^^^^^^^
+WARNING notNullSafety: potential null array access in parameter string of function ltrim at testdata/mustache/src/Mustache/Loader/FilesystemLoader.php:64
+                $this->extension = '.' . ltrim($options['extension'], '.');
+                                               ^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: null passed to non-nullable parameter context in function file_get_contents at testdata/mustache/src/Mustache/Loader/InlineLoader.php:114
+            $data = file_get_contents($this->fileName, false, null, $this->offset);
+                                                              ^^^^
 WARNING regexpVet: '\w' intersects with '\d' in [\w\d\.] at testdata/mustache/src/Mustache/Loader/InlineLoader.php:115
             foreach (preg_split("/^@@(?= [\w\d\.]+$)/m", $data, -1) as $chunk) {
                                 ^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null dereference when accessing property 'stream' at testdata/mustache/src/Mustache/Logger/StreamLogger.php:61
+            fclose($this->stream);
+                   ^^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function array_key_exists signature of param key at testdata/mustache/src/Mustache/Logger/StreamLogger.php:102
+        if (!array_key_exists($level, self::$levels)) {
+                              ^^^^^^
+WARNING notNullSafety: not null safety call in function writeLog signature of param level at testdata/mustache/src/Mustache/Logger/StreamLogger.php:107
+            $this->writeLog($level, $message, $context);
+                            ^^^^^^
+WARNING notNullSafety: potential null dereference when accessing property 'url' at testdata/mustache/src/Mustache/Logger/StreamLogger.php:128
+            $this->stream = fopen($this->url, 'a');
+                                  ^^^^^^^^^^
+WARNING notNullSafety: potential null dereference when accessing property 'stream' at testdata/mustache/src/Mustache/Logger/StreamLogger.php:136
+        fwrite($this->stream, self::formatLine($level, $message, $context));
+               ^^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function enablePragma signature of param name at testdata/mustache/src/Mustache/Parser.php:58
+            $this->enablePragma($pragma);
+                                ^^^^^^^
+WARNING notNullSafety: potential null array access in parameter name of function getNameAndFilters at testdata/mustache/src/Mustache/Parser.php:88
+                list($name, $filters) = $this->getNameAndFilters($token[Mustache_Tokenizer::NAME]);
+                                                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter name of function enablePragma at testdata/mustache/src/Mustache/Parser.php:167
+                    $this->enablePragma($token[Mustache_Tokenizer::NAME]);
+                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter string of function substr at testdata/mustache/src/Mustache/Parser.php:235
+                if (substr($next[Mustache_Tokenizer::VALUE], -1) !== "\n") {
+                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: potential null array access in parameter subject of function preg_match at testdata/mustache/src/Mustache/Parser.php:262
+            return preg_match('/^\s*$/', $token[Mustache_Tokenizer::VALUE]);
+                                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 WARNING switchDefault: Add 'default' branch to avoid unexpected unhandled condition values at testdata/mustache/src/Mustache/Parser.php:307
         switch ($name) {
         ^
@@ -190,3 +322,15 @@ WARNING errorSilence: Don't use @, silencing errors is bad practice at testdata/
 WARNING unused: Variable $v is unused (use $_ to ignore this inspection or specify --unused-var-regex flag) at testdata/mustache/src/Mustache/Template.php:122
                 foreach ($value as $k => $v) {
                                          ^^
+WARNING notNullSafety: not null safety call in function call_user_func signature of param callback at testdata/mustache/src/Mustache/Template.php:174
+                ->loadLambda((string) call_user_func($value))
+                                                     ^^^^^^
+WARNING notNullSafety: not null safety call in function trim signature of param string at testdata/mustache/src/Mustache/Tokenizer.php:110
+        if ($delimiters = trim($delimiters)) {
+                               ^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function substr signature of param string at testdata/mustache/src/Mustache/Tokenizer.php:188
+                                if (substr($lastName, -1) === '}') {
+                                           ^^^^^^^^^
+WARNING notNullSafety: not null safety call in function substr signature of param string at testdata/mustache/src/Mustache/Tokenizer.php:189
+                                    $token[self::NAME] = trim(substr($lastName, 0, -1));
+                                                                     ^^^^^^^^^
diff --git a/src/tests/golden/testdata/qrcode/golden.txt b/src/tests/golden/testdata/qrcode/golden.txt
index e6b2f8e..e201c4c 100644
--- a/src/tests/golden/testdata/qrcode/golden.txt
+++ b/src/tests/golden/testdata/qrcode/golden.txt
@@ -4,12 +4,33 @@ MAYBE   missingPhpdoc: Missing PHPDoc for \QRCode::output_image public method at
 MAYBE   missingPhpdoc: Missing PHPDoc for \QRCode::render_image public method at testdata/qrcode/qrcode.php:53
   public function render_image() {
                   ^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function imagecreatetruecolor signature of param width at testdata/qrcode/qrcode.php:56
+    $image = imagecreatetruecolor($width, $height);
+                                  ^^^^^^
+WARNING notNullSafety: not null safety call in function imagecreatetruecolor signature of param height at testdata/qrcode/qrcode.php:56
+    $image = imagecreatetruecolor($width, $height);
+                                          ^^^^^^^
 MAYBE   ternarySimplify: Could rewrite as `$this->options['bc'] ?? 'FFFFFF'` at testdata/qrcode/qrcode.php:59
     $bgcolor = (isset($this->options['bc']) ? $this->options['bc'] : 'FFFFFF');
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 MAYBE   ternarySimplify: Could rewrite as `$this->options['fc'] ?? '000000'` at testdata/qrcode/qrcode.php:63
     $fgcolor = (isset($this->options['fc']) ? $this->options['fc'] : '000000');
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function floor signature of param num at testdata/qrcode/qrcode.php:72
+      $scale = (($scale > 1) ? floor($scale) : 1);
+                                     ^^^^^^
+WARNING notNullSafety: not null safety call in function substr signature of param string at testdata/qrcode/qrcode.php:134
+    $r     = hexdec(substr($color, 0, 2));
+                           ^^^^^^
+WARNING notNullSafety: not null safety call in function substr signature of param string at testdata/qrcode/qrcode.php:135
+    $g     = hexdec(substr($color, 2, 2));
+                           ^^^^^^
+WARNING notNullSafety: not null safety call in function substr signature of param string at testdata/qrcode/qrcode.php:136
+    $b     = hexdec(substr($color, 4, 2));
+                           ^^^^^^
+WARNING notNullSafety: not null safety call in function imagecolorallocate signature of param image at testdata/qrcode/qrcode.php:137
+    return imagecolorallocate($image, $r, $g, $b);
+                              ^^^^^^
 WARNING deadCode: Unreachable code at testdata/qrcode/qrcode.php:155
     return null;
            ^^^^
@@ -19,12 +40,30 @@ MAYBE   trailingComma: Last element in a multi-line array should have a trailing
 WARNING unused: Variable $mode is unused (use $_ to ignore this inspection or specify --unused-var-regex flag) at testdata/qrcode/qrcode.php:177
     list($mode, $vers, $ec, $data) = $this->qr_encode_data($data, $ecl);
          ^^^^^
+WARNING notNullSafety: not null safety call in function substr signature of param string at testdata/qrcode/qrcode.php:200
+    $data = substr($data, 0, $max_chars);
+                   ^^^^^
 WARNING switchDefault: Add 'default' branch to avoid unexpected unhandled condition values at testdata/qrcode/qrcode.php:203
     switch ($mode) {
     ^
 WARNING maybeUndefined: Possibly undefined variable $code at testdata/qrcode/qrcode.php:221
     while (count($code) % 8) {
                  ^^^^^
+WARNING notNullSafety: not null safety call in function preg_match signature of param subject at testdata/qrcode/qrcode.php:268
+    if (preg_match($numeric, $data)) {
+                             ^^^^^
+WARNING notNullSafety: not null safety call in function preg_match signature of param subject at testdata/qrcode/qrcode.php:271
+    if (preg_match($alphanumeric, $data)) {
+                                  ^^^^^
+WARNING notNullSafety: not null safety call in function preg_match signature of param subject at testdata/qrcode/qrcode.php:274
+    if (preg_match($kanji, $data)) {
+                           ^^^^^
+WARNING notNullSafety: not null safety call in function strlen signature of param string at testdata/qrcode/qrcode.php:281
+    $length = strlen($data);
+                     ^^^^^
+WARNING notNullSafety: not null safety call in function strlen signature of param string at testdata/qrcode/qrcode.php:295
+    $length = strlen($data);
+                     ^^^^^
 WARNING caseBreak: Add break or '// fallthrough' to the end of the case at testdata/qrcode/qrcode.php:297
       case 2:  /* 27 - 40 */
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -34,6 +73,9 @@ WARNING caseBreak: Add break or '// fallthrough' to the end of the case at testd
 WARNING switchDefault: Add 'default' branch to avoid unexpected unhandled condition values at testdata/qrcode/qrcode.php:296
     switch ($version_group) {
     ^
+WARNING notNullSafety: not null safety call in function substr signature of param string at testdata/qrcode/qrcode.php:316
+      $group = substr($data, $i, 3);
+                      ^^^^^
 WARNING caseBreak: Add break or '// fallthrough' to the end of the case at testdata/qrcode/qrcode.php:318
         case 3:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -43,6 +85,9 @@ WARNING caseBreak: Add break or '// fallthrough' to the end of the case at testd
 WARNING switchDefault: Add 'default' branch to avoid unexpected unhandled condition values at testdata/qrcode/qrcode.php:317
       switch (strlen($group)) {
       ^
+WARNING notNullSafety: not null safety call in function strlen signature of param string at testdata/qrcode/qrcode.php:339
+    $length   = strlen($data);
+                       ^^^^^
 WARNING caseBreak: Add break or '// fallthrough' to the end of the case at testdata/qrcode/qrcode.php:341
       case 2:  /* 27 - 40 */
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -52,21 +97,33 @@ WARNING caseBreak: Add break or '// fallthrough' to the end of the case at testd
 WARNING switchDefault: Add 'default' branch to avoid unexpected unhandled condition values at testdata/qrcode/qrcode.php:340
     switch ($version_group) {
     ^
+WARNING notNullSafety: not null safety call in function substr signature of param string at testdata/qrcode/qrcode.php:359
+      $group = substr($data, $i, 2);
+                      ^^^^^
 MAYBE   callSimplify: Could simplify to $group[0] at testdata/qrcode/qrcode.php:361
         $c1     = strpos($alphabet, substr($group, 0, 1));
                                     ^^^^^^^^^^^^^^^^^^^^
 MAYBE   callSimplify: Could simplify to $group[1] at testdata/qrcode/qrcode.php:362
         $c2     = strpos($alphabet, substr($group, 1, 1));
                                     ^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function strlen signature of param string at testdata/qrcode/qrcode.php:390
+    $length = strlen($data);
+                     ^^^^^
 WARNING caseBreak: Add break or '// fallthrough' to the end of the case at testdata/qrcode/qrcode.php:393
       case 1:  /* 10 - 26 */
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 WARNING switchDefault: Add 'default' branch to avoid unexpected unhandled condition values at testdata/qrcode/qrcode.php:391
     switch ($version_group) {
     ^
+WARNING notNullSafety: not null safety call in function substr signature of param string at testdata/qrcode/qrcode.php:413
+      $ch     = ord(substr($data, $i, 1));
+                           ^^^^^
 MAYBE   callSimplify: Could simplify to $data[$i] at testdata/qrcode/qrcode.php:413
       $ch     = ord(substr($data, $i, 1));
                     ^^^^^^^^^^^^^^^^^^^^
+WARNING notNullSafety: not null safety call in function strlen signature of param string at testdata/qrcode/qrcode.php:428
+    $length = strlen($data);
+                     ^^^^^
 WARNING caseBreak: Add break or '// fallthrough' to the end of the case at testdata/qrcode/qrcode.php:430
       case 2:  /* 27 - 40 */
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -76,6 +133,9 @@ WARNING caseBreak: Add break or '// fallthrough' to the end of the case at testd
 WARNING switchDefault: Add 'default' branch to avoid unexpected unhandled condition values at testdata/qrcode/qrcode.php:429
     switch ($version_group) {
     ^
+WARNING notNullSafety: not null safety call in function substr signature of param string at testdata/qrcode/qrcode.php:447
+      $group = substr($data, $i, 2);
+                      ^^^^^
 MAYBE   callSimplify: Could simplify to $group[0] at testdata/qrcode/qrcode.php:448
       $c1    = ord(substr($group, 0, 1));
                    ^^^^^^^^^^^^^^^^^^^^