Repository: VKCOM/noverify
Pull request #1235 (2024-08-08 19:44:22)

inspection: types nullable


Patch:
diff --git a/docs/checkers_doc.md b/docs/checkers_doc.md
index fea7b9d..166416b 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 |
 | ------------ | ------------------------- | -------------------------- | ------------------ |
-| 103          | 85                        | 18                         | 13                 |
+| 104           | 86                        | 18                         | 14                 |
 
 ## Table of contents
  - Enabled by default
@@ -59,6 +59,7 @@
    - [`nestedTernary` checker](#nestedternary-checker)
    - [`newAbstract` checker](#newabstract-checker)
    - [`nonPublicInterfaceMember` checker](#nonpublicinterfacemember-checker)
+   - [`notExplicitNullableParam` checker (autofixable)](#notexplicitnullableparam-checker)
    - [`offBy1` checker (autofixable)](#offby1-checker)
    - [`oldStyleConstructor` checker](#oldstyleconstructor-checker)
    - [`paramClobber` checker](#paramclobber-checker)
@@ -1176,6 +1177,26 @@ interface Iface {
 <p><br></p>
 
 
+### `notExplicitNullableParam` checker
+
+> Auto fix available
+
+#### Description
+
+Report not nullable param can be null.
+
+#### Non-compliant code:
+```php
+function f(string $str = null);
+```
+
+#### Compliant code:
+```php
+function f(?string $str = null);
+```
+<p><br></p>
+
+
 ### `offBy1` checker
 
 > Auto fix available
diff --git a/src/linter/block.go b/src/linter/block.go
index ee1401c..fa3a95f 100644
--- a/src/linter/block.go
+++ b/src/linter/block.go
@@ -551,8 +551,44 @@ func (b *blockWalker) handleAndCheckGlobalStmt(s *ir.GlobalStmt) {
 	}
 }
 
+func (b *blockWalker) CheckParamNullability(params []ir.Node) {
+	for _, param := range params {
+		if p, ok := param.(*ir.Parameter); ok {
+			var paramType ir.Node
+			paramType, paramOk := p.VariableType.(*ir.Name)
+			if !paramOk {
+				paramIdentifier, paramIdentifierOk := p.VariableType.(*ir.Identifier)
+				if !paramIdentifierOk {
+					continue
+				}
+				paramType = paramIdentifier
+			}
+
+			paramName, ok := paramType.(*ir.Name)
+			if ok {
+				if paramName.Value == "mixed" {
+					continue
+				}
+			}
+
+			defValue, defValueOk := p.DefaultValue.(*ir.ConstFetchExpr)
+			if !defValueOk {
+				continue
+			}
+
+			if defValue.Constant.Value != "null" {
+				continue
+			}
+
+			b.linter.report(paramType, LevelWarning, "notExplicitNullableParam", "parameter with null default value should be explicitly nullable")
+			b.r.addQuickFix("notExplicitNullableParam", b.linter.quickfix.notExplicitNullableParam(paramType))
+		}
+	}
+}
+
 func (b *blockWalker) handleFunction(fun *ir.FunctionStmt) bool {
 	if b.ignoreFunctionBodies {
+		b.CheckParamNullability(fun.Params)
 		return false
 	}
 
@@ -1034,6 +1070,7 @@ func (b *blockWalker) handleCallArgs(args []ir.Node, fn meta.FuncInfo) {
 				ArgTypes: funcArgTypes,
 			}
 
+			b.CheckParamNullability(a.Params)
 			b.enterClosure(a, isInstance, typ, closureSolver)
 		default:
 			a.Walk(b)
diff --git a/src/linter/block_linter.go b/src/linter/block_linter.go
index bd7bc36..3f6cbd4 100644
--- a/src/linter/block_linter.go
+++ b/src/linter/block_linter.go
@@ -33,9 +33,18 @@ func (b *blockLinter) enterNode(n ir.Node) {
 	case *ir.ClassStmt:
 		b.checkClass(n)
 
+	case *ir.TraitStmt:
+		b.checkTrait(n)
+
 	case *ir.FunctionCallExpr:
 		b.checkFunctionCall(n)
 
+	case *ir.ArrowFunctionExpr:
+		b.walker.CheckParamNullability(n.Params)
+
+	case *ir.ClosureExpr:
+		b.walker.CheckParamNullability(n.Params)
+
 	case *ir.MethodCallExpr:
 		b.checkMethodCall(n)
 
@@ -207,15 +216,25 @@ func (b *blockLinter) checkUnaryPlus(n *ir.UnaryPlusExpr) {
 	b.report(n, LevelWarning, "strangeCast", "Unary plus with non-constant expression, possible type cast, use an explicit cast to int or float instead of using the unary plus")
 }
 
+func (b *blockLinter) checkTrait(n *ir.TraitStmt) {
+	for _, stmt := range n.Stmts {
+		method, ok := stmt.(*ir.ClassMethodStmt)
+		if ok {
+			b.walker.CheckParamNullability(method.Params)
+		}
+	}
+}
+
 func (b *blockLinter) checkClass(class *ir.ClassStmt) {
 	const classMethod = 0
 	const classOtherMember = 1
 
 	var members = make([]int, 0, len(class.Stmts))
 	for _, stmt := range class.Stmts {
-		switch stmt.(type) {
+		switch stmt := stmt.(type) {
 		case *ir.ClassMethodStmt:
 			members = append(members, classMethod)
+			b.walker.CheckParamNullability(stmt.Params)
 		default:
 			members = append(members, classOtherMember)
 		}
@@ -1223,7 +1242,6 @@ func (b *blockLinter) checkStripTags(e *ir.FunctionCallExpr) {
 
 func (b *blockLinter) checkMethodCall(e *ir.MethodCallExpr) {
 	parseState := b.classParseState()
-
 	call := resolveMethodCall(b.walker.ctx.sc, parseState, b.walker.ctx.customTypes, e, b.walker.r.strictMixed)
 	if !call.canAnalyze {
 		return
@@ -1433,6 +1451,7 @@ func (b *blockLinter) checkInterfaceStmt(iface *ir.InterfaceStmt) {
 					b.report(x, LevelWarning, "nonPublicInterfaceMember", "'%s' can't be %s", methodName, modifier.Value)
 				}
 			}
+			b.walker.CheckParamNullability(x.Params)
 		case *ir.ClassConstListStmt:
 			for _, modifier := range x.Modifiers {
 				if strings.EqualFold(modifier.Value, "private") || strings.EqualFold(modifier.Value, "protected") {
diff --git a/src/linter/quickfix.go b/src/linter/quickfix.go
index ef83aa8..5c9b3bf 100644
--- a/src/linter/quickfix.go
+++ b/src/linter/quickfix.go
@@ -7,6 +7,7 @@ import (
 	"github.com/VKCOM/noverify/src/ir"
 	"github.com/VKCOM/noverify/src/quickfix"
 	"github.com/VKCOM/noverify/src/workspace"
+	"github.com/VKCOM/php-parser/pkg/position"
 )
 
 type QuickFixGenerator struct {
@@ -47,6 +48,32 @@ func (g *QuickFixGenerator) NullForNotNullableProperty(prop *ir.PropertyStmt) qu
 	}
 }
 
+func (g *QuickFixGenerator) notExplicitNullableParam(param ir.Node) quickfix.TextEdit {
+	var pos *position.Position
+	var value string
+
+	switch v := param.(type) {
+	case *ir.Name:
+		pos = &position.Position{
+			StartPos: v.Position.StartPos,
+			EndPos:   v.Position.EndPos,
+		}
+		value = v.Value
+	case *ir.Identifier:
+		pos = &position.Position{
+			StartPos: v.Position.StartPos,
+			EndPos:   v.Position.EndPos,
+		}
+		value = v.Value
+	}
+
+	return quickfix.TextEdit{
+		StartPos:    pos.StartPos,
+		EndPos:      pos.EndPos,
+		Replacement: "?" + value,
+	}
+}
+
 func (g *QuickFixGenerator) GetType(node ir.Node, isFunctionName, nodeText string, isNegative bool) quickfix.TextEdit {
 	pos := ir.GetPosition(node)
 
diff --git a/src/linter/report.go b/src/linter/report.go
index d1914da..b36363b 100644
--- a/src/linter/report.go
+++ b/src/linter/report.go
@@ -25,6 +25,15 @@ func addBuiltinCheckers(reg *CheckersRegistry) {
 			After:    `$s = strip_tags($s, '<br>')`,
 		},
 
+		{
+			Name:     "notExplicitNullableParam",
+			Default:  true,
+			Quickfix: true,
+			Comment:  "Report not nullable param with explicit null default value.",
+			Before:   `function f(string $str = null);`,
+			After:    `function f(?string $str = null);`,
+		},
+
 		{
 			Name:     "emptyStmt",
 			Default:  true,
diff --git a/src/linter/root_checker.go b/src/linter/root_checker.go
index 149b767..f7b12e0 100644
--- a/src/linter/root_checker.go
+++ b/src/linter/root_checker.go
@@ -604,7 +604,6 @@ func (r *rootChecker) checkFuncParam(p *ir.Parameter) {
 		}
 		return true
 	})
-
 	r.CheckTypeHintFunctionParam(p)
 }
 


Test patch:
diff --git a/src/tests/checkers/not_explicit_nullable_types_test.go b/src/tests/checkers/not_explicit_nullable_types_test.go
new file mode 100644
index 0000000..e48d8bf
--- /dev/null
+++ b/src/tests/checkers/not_explicit_nullable_types_test.go
@@ -0,0 +1,334 @@
+package checkers
+
+import (
+	"testing"
+
+	"github.com/VKCOM/noverify/src/linttest"
+)
+
+func TestNotNullableString(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+function nullableString(?string $a = null) {}
+`)
+
+	test.RunAndMatch()
+}
+
+func TestNotNullableArray(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+/** @param string[] array */
+function nullableArray(array $a = null) {}
+`)
+
+	test.Expect = []string{
+		"parameter with null default value should be explicitly nullable",
+	}
+	test.RunAndMatch()
+}
+
+func TestNullableCallable(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+function nullableCallable(?callable $a = null) {}
+`)
+
+	test.RunAndMatch()
+}
+
+func TestNotNullableCallable(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+function NotNullableCallable(callable $a = null) {}
+`)
+
+	test.Expect = []string{
+		"parameter with null default value should be explicitly nullable",
+	}
+	test.RunAndMatch()
+}
+
+func TestNotNullableClasses(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+class MyClass1 {}
+
+class MyClass2 {
+    public function myMethod(MyClass1 $a = null) {}
+}
+`)
+
+	test.Expect = []string{
+		"parameter with null default value should be explicitly nullable",
+		"Missing PHPDoc for",
+	}
+	test.RunAndMatch()
+}
+
+func TestNullableClasses(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+class MyClass1 {}
+
+class MyClass2 {
+    public function myMethod(?MyClass1 $a = null) {}
+}
+`)
+
+	test.Expect = []string{
+		"Missing PHPDoc for",
+	}
+	test.RunAndMatch()
+}
+
+func TestNullableMultipleArgs(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+function multipleArgsExample(?string $a, ?int $b = null, ?bool $c = null) {}
+`)
+
+	test.RunAndMatch()
+}
+
+func TestNotNullableMultipleArgs(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+function multipleArgsExample(string $a, int $b = null, bool $c = null) {}
+`)
+
+	test.Expect = []string{
+		"parameter with null default value should be explicitly nullable",
+		"parameter with null default value should be explicitly nullable",
+	}
+
+	test.RunAndMatch()
+}
+
+func TestNullableOrString(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+function nullableOrString(null|string $a = null) {}
+`)
+
+	test.RunAndMatch()
+}
+
+func TestNullableOrClass(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+class MyClass1 {}
+
+class MyClass2 {
+    public function myMethod(null|MyClass1 $a = null) {}
+}
+`)
+
+	test.Expect = []string{
+		"Missing PHPDoc for \\MyClass2::myMethod public method",
+	}
+
+	test.RunAndMatch()
+}
+
+func TestMixedParam(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+function mixedParam(mixed $a = null) {}
+`)
+
+	test.RunAndMatch()
+}
+
+func TestTraitMethodNullable(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+trait MyTrait {
+    public function myMethod(?string $a = null) {}
+}
+`)
+
+	test.Expect = []string{
+		"Missing PHPDoc for \\MyTrait::myMethod public method",
+	}
+
+	test.RunAndMatch()
+}
+
+func TestTraitMethodNotNullable(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+trait MyTrait {
+    public function myMethod(string $a = null) {}
+}
+`)
+
+	test.Expect = []string{
+		"parameter with null default value should be explicitly nullable",
+		"Missing PHPDoc for \\MyTrait::myMethod public method",
+	}
+
+	test.RunAndMatch()
+}
+
+func TestInterfaceMethodNullable(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+interface MyInterface {
+    public function myMethod(?string $a = null);
+}
+`)
+
+	test.RunAndMatch()
+}
+
+func TestInterfaceMethodNotNullable(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+interface MyInterface {
+    public function myMethod(string $a = null);
+}
+`)
+
+	test.Expect = []string{
+		"parameter with null default value should be explicitly nullable",
+	}
+
+	test.RunAndMatch()
+}
+
+func TestClosureNullable(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+$closure = function(?string $a = null) {};
+`)
+
+	test.RunAndMatch()
+}
+
+func TestClosureNotNullable(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+$closure = function(string $a = null) {};
+`)
+
+	test.Expect = []string{
+		"parameter with null default value should be explicitly nullable",
+	}
+
+	test.RunAndMatch()
+}
+
+func TestArrowFunctionNullable(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+$closure_fn = fn(?string $a = null) => null;
+`)
+
+	test.RunAndMatch()
+}
+
+func TestArrowFunctionNotNullable(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+$closure_fn = fn(string $a = null) => null;
+`)
+
+	test.Expect = []string{
+		"parameter with null default value should be explicitly nullable",
+	}
+
+	test.RunAndMatch()
+}
+
+func TestArrowFunctionClassNullable(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+class MyClass {}
+
+$closure_fn = fn(?MyClass $a = null) => null;
+`)
+
+	test.RunAndMatch()
+}
+
+func TestArrowFunctionClassNotNullable(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+class MyClass {}
+
+$closure_fn = fn(MyClass $a = null) => null;
+`)
+
+	test.Expect = []string{
+		"parameter with null default value should be explicitly nullable",
+	}
+
+	test.RunAndMatch()
+}
+
+func TestArrowFunctionArrayNullable(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+$closure_fn = fn(?array $a = null) => null;
+`)
+
+	test.RunAndMatch()
+}
+
+func TestArrowFunctionArrayNotNullable(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+$closure_fn = fn(array $a = null) => null;
+`)
+
+	test.Expect = []string{
+		"parameter with null default value should be explicitly nullable",
+	}
+
+	test.RunAndMatch()
+}
+
+func TestArrowFunctionMultipleParams(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+$closure_fn = fn(?string $a, int $b = null, ?bool $c = null) => null;
+`)
+
+	test.Expect = []string{
+		"parameter with null default value should be explicitly nullable",
+	}
+
+	test.RunAndMatch()
+}
+
+func TestClassWithFunCallback(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+class SomeClass {
+
+    public function willReturnCallback($callback): self
+    {
+        return $this;
+    }
+
+ private function funWithCallback(array $participation_statuses_data) {
+    $this
+      ->willReturnCallback(static function(int $user_id, array $exact_statuses = [], bool $need_hidden = true, Date $finished_at = null) use ($participation_statuses_data) {
+            return;
+      });
+  }
+}
+`)
+
+	test.Expect = []string{
+		"parameter with null default value should be explicitly nullable",
+		"Missing PHPDoc for \\SomeClass::willReturnCallback public method",
+		"Specify the type for the parameter $participation_statuses_data",
+		"Expression evaluated but not used",
+		"Variable $participation_statuses_data is unused",
+		"Class or interface named \\Date does not exist",
+	}
+
+	test.RunAndMatch()
+}
diff --git a/src/tests/golden/testdata/flysystem/golden.txt b/src/tests/golden/testdata/flysystem/golden.txt
index 9378e75..1391a8a 100644
--- a/src/tests/golden/testdata/flysystem/golden.txt
+++ b/src/tests/golden/testdata/flysystem/golden.txt
@@ -61,6 +61,12 @@ MAYBE   missingPhpdoc: Missing PHPDoc for \League\Flysystem\Adapter\Polyfill\Str
 MAYBE   missingPhpdoc: Missing PHPDoc for \League\Flysystem\Adapter\Polyfill\StreamedWritingTrait::update public method at testdata/flysystem/src/Adapter/Polyfill/StreamedWritingTrait.php:59
     abstract public function update($pash, $contents, Config $config);
                              ^^^^^^
+WARNING notExplicitNullableParam: parameter with null default value should be explicitly nullable at testdata/flysystem/src/FileExistsException.php:21
+    public function __construct($path, $code = 0, BaseException $previous = null)
+                                                  ^^^^^^^^^^^^^
+WARNING notExplicitNullableParam: parameter with null default value should be explicitly nullable at testdata/flysystem/src/FileNotFoundException.php:21
+    public function __construct($path, $code = 0, BaseException $previous = null)
+                                                  ^^^^^^^^^^^^^
 MAYBE   typeHint: Specify the type for the parameter $config in PHPDoc, 'array' type hint too generic at testdata/flysystem/src/Filesystem.php:63
     public function write($path, $contents, array $config = [])
                     ^^^^^
@@ -82,15 +88,27 @@ MAYBE   typeHint: Specify the type for the parameter $config in PHPDoc, 'array'
 MAYBE   typeHint: Specify the type for the parameter $config in PHPDoc, 'array' type hint too generic at testdata/flysystem/src/Filesystem.php:257
     public function createDir($dirname, array $config = [])
                     ^^^^^^^^^
+WARNING notExplicitNullableParam: parameter with null default value should be explicitly nullable at testdata/flysystem/src/Filesystem.php:362
+    public function get($path, Handler $handler = null)
+                               ^^^^^^^
+WARNING notExplicitNullableParam: parameter with null default value should be explicitly nullable at testdata/flysystem/src/FilesystemInterface.php:274
+    public function get($path, Handler $handler = null);
+                               ^^^^^^^
 WARNING unused: Variable $e is unused (use $_ to ignore this inspection or specify --unused-var-regex flag) at testdata/flysystem/src/Handler.php:129
         } catch (BadMethodCallException $e) {
                                         ^^
+WARNING notExplicitNullableParam: parameter with null default value should be explicitly nullable at testdata/flysystem/src/Handler.php:28
+    public function __construct(FilesystemInterface $filesystem = null, $path = null)
+                                ^^^^^^^^^^^^^^^^^^^
 WARNING unused: Variable $e is unused (use $_ to ignore this inspection or specify --unused-var-regex flag) at testdata/flysystem/src/MountManager.php:275
         } catch (PluginNotFoundException $e) {
                                          ^^
 MAYBE   deprecatedUntagged: Call to deprecated method {\League\Flysystem\FilesystemInterface}->get() at testdata/flysystem/src/MountManager.php:646
         return $this->getFilesystem($prefix)->get($path);
                                               ^^^
+WARNING notExplicitNullableParam: parameter with null default value should be explicitly nullable at testdata/flysystem/src/MountManager.php:642
+    public function get($path, Handler $handler = null)
+                               ^^^^^^^
 WARNING unused: Variable $e is unused (use $_ to ignore this inspection or specify --unused-var-regex flag) at testdata/flysystem/src/Plugin/ForcedCopy.php:33
         } catch (FileNotFoundException $e) {
                                        ^^
diff --git a/src/tests/golden/testdata/mustache/golden.txt b/src/tests/golden/testdata/mustache/golden.txt
index dfdf00a..c5667a9 100644
--- a/src/tests/golden/testdata/mustache/golden.txt
+++ b/src/tests/golden/testdata/mustache/golden.txt
@@ -151,21 +151,39 @@ 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 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)
+                                         ^^^^^^^^^^^^^^
+WARNING notExplicitNullableParam: parameter with null default value should be explicitly nullable at testdata/mustache/src/Mustache/Exception/SyntaxException.php:24
+    public function __construct($msg, array $token, Exception $previous = null)
+                                                    ^^^^^^^^^
 MAYBE   missingPhpdoc: Missing PHPDoc for \Mustache_Exception_UnknownFilterException::getFilterName public method at testdata/mustache/src/Mustache/Exception/UnknownFilterException.php:34
     public function getFilterName()
                     ^^^^^^^^^^^^^
+WARNING notExplicitNullableParam: parameter with null default value should be explicitly nullable at testdata/mustache/src/Mustache/Exception/UnknownFilterException.php:23
+    public function __construct($filterName, Exception $previous = null)
+                                             ^^^^^^^^^
 MAYBE   missingPhpdoc: Missing PHPDoc for \Mustache_Exception_UnknownHelperException::getHelperName public method at testdata/mustache/src/Mustache/Exception/UnknownHelperException.php:34
     public function getHelperName()
                     ^^^^^^^^^^^^^
+WARNING notExplicitNullableParam: parameter with null default value should be explicitly nullable at testdata/mustache/src/Mustache/Exception/UnknownHelperException.php:23
+    public function __construct($helperName, Exception $previous = null)
+                                             ^^^^^^^^^
 MAYBE   missingPhpdoc: Missing PHPDoc for \Mustache_Exception_UnknownTemplateException::getTemplateName public method at testdata/mustache/src/Mustache/Exception/UnknownTemplateException.php:34
     public function getTemplateName()
                     ^^^^^^^^^^^^^^^
+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 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 switchDefault: Add 'default' branch to avoid unexpected unhandled condition values at testdata/mustache/src/Mustache/Parser.php:307
         switch ($name) {
         ^
+WARNING notExplicitNullableParam: parameter with null default value should be explicitly nullable at testdata/mustache/src/Mustache/Parser.php:73
+    private function buildTree(array &$tokens, array $parent = null)
+                                               ^^^^^
 WARNING errorSilence: Don't use @, silencing errors is bad practice at testdata/mustache/src/Mustache/Source/FilesystemSource.php:53
                 $this->stat = @stat($this->fileName);
                               ^^^^^^^^^^^^^^^^^^^^^^
diff --git a/src/tests/golden/testdata/parsedown/golden.txt b/src/tests/golden/testdata/parsedown/golden.txt
index c061c1f..5a07900 100644
--- a/src/tests/golden/testdata/parsedown/golden.txt
+++ b/src/tests/golden/testdata/parsedown/golden.txt
@@ -172,6 +172,15 @@ MAYBE   typeHint: Specify the type for the parameter $Element in PHPDoc, 'array'
 MAYBE   implicitModifiers: Specify the access modifier for \Parsedown::instance method explicitly at testdata/parsedown/parsedown.php:1938
     static function instance($name = 'default')
                     ^^^^^^^^
+WARNING notExplicitNullableParam: parameter with null default value should be explicitly nullable at testdata/parsedown/parsedown.php:574
+    protected function blockList($Line, array $CurrentBlock = null)
+                                        ^^^^^
+WARNING notExplicitNullableParam: parameter with null default value should be explicitly nullable at testdata/parsedown/parsedown.php:811
+    protected function blockSetextHeader($Line, array $Block = null)
+                                                ^^^^^
+WARNING notExplicitNullableParam: parameter with null default value should be explicitly nullable at testdata/parsedown/parsedown.php:897
+    protected function blockTable($Line, array $Block = null)
+                                         ^^^^^
 ERROR   classMembersOrder: Property $breaksEnabled must go before methods in the class Parsedown at testdata/parsedown/parsedown.php:66
     protected $breaksEnabled;
     ^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/src/tests/golden/testdata/quickfix/notExplicitNullableParam.php b/src/tests/golden/testdata/quickfix/notExplicitNullableParam.php
new file mode 100644
index 0000000..2cd6852
--- /dev/null
+++ b/src/tests/golden/testdata/quickfix/notExplicitNullableParam.php
@@ -0,0 +1,34 @@
+<?php
+
+function notNullableString(string $filed = null) {}
+
+function notNullableCallable(callable $a = null) {}
+
+class MyClass1 {}
+
+class MyClass2 {
+    public function myMethod(MyClass1 $a = null) {}
+}
+
+function nullableArray(array $a = null) {}
+
+function multipleArgsExample(string $a, int $b = null, bool $c = null) {}
+
+function nullableOrString(null|string $a = null) {}
+
+function mixedParam(mixed $a = null) {}
+
+class SomeClass {
+
+    public function willReturnCallback($callback): self
+    {
+        return $this;
+    }
+
+ private function funWithCallback(array $participation_statuses_data) {
+    $this
+      ->willReturnCallback(static function(int $user_id, array $exact_statuses = [], bool $need_hidden = true, Date $finished_at = null) use ($participation_statuses_data) {
+            return;
+      });
+  }
+}
diff --git a/src/tests/golden/testdata/quickfix/notExplicitNullableParam.php.fix.expected b/src/tests/golden/testdata/quickfix/notExplicitNullableParam.php.fix.expected
new file mode 100644
index 0000000..649de6a
--- /dev/null
+++ b/src/tests/golden/testdata/quickfix/notExplicitNullableParam.php.fix.expected
@@ -0,0 +1,34 @@
+<?php
+
+function notNullableString(?string $filed = null) {}
+
+function notNullableCallable(?callable $a = null) {}
+
+class MyClass1 {}
+
+class MyClass2 {
+    public function myMethod(?MyClass1 $a = null) {}
+}
+
+function nullableArray(?array $a = null) {}
+
+function multipleArgsExample(string $a, ?int $b = null, ?bool $c = null) {}
+
+function nullableOrString(null|string $a = null) {}
+
+function mixedParam(mixed $a = null) {}
+
+class SomeClass {
+
+    public function willReturnCallback($callback): self
+    {
+        return $this;
+    }
+
+ private function funWithCallback(array $participation_statuses_data) {
+    $this
+      ->willReturnCallback(static function(int $user_id, array $exact_statuses = [], bool $need_hidden = true, ?Date $finished_at = null) use ($participation_statuses_data) {
+            return;
+      });
+  }
+}