Repository: VKCOM/noverify
Pull request #1219 (2024-05-03 13:06:16)

linter: new inspection for php functions aliases


Patch:
diff --git a/docs/checkers_doc.md b/docs/checkers_doc.md
index bb31729..1b67701 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 |
 | ------------ | ------------------------- | -------------------------- | ------------------ |
-| 105           | 87                        | 18                         | 14                 |
+| 106           | 88                        | 18                         | 15                 |
 
 ## Table of contents
  - Enabled by default
@@ -64,6 +64,7 @@
    - [`oldStyleConstructor` checker](#oldstyleconstructor-checker)
    - [`paramClobber` checker](#paramclobber-checker)
    - [`parentConstructor` checker](#parentconstructor-checker)
+   - [`phpAliases` checker (autofixable)](#phpaliases-checker)
    - [`precedence` checker](#precedence-checker)
    - [`printf` checker](#printf-checker)
    - [`redundantGlobal` checker](#redundantglobal-checker)
@@ -1294,6 +1295,26 @@ class Foo extends Bar {
 <p><br></p>
 
 
+### `phpAliases` checker
+
+> Auto fix available
+
+#### Description
+
+Report php aliases functions.
+
+#### Non-compliant code:
+```php
+join("", []);
+```
+
+#### Compliant code:
+```php
+implode("", []);
+```
+<p><br></p>
+
+
 ### `precedence` checker
 
 #### Description
diff --git a/src/ir/phpcore/phpcore.go b/src/ir/phpcore/phpcore.go
index 5729b5d..b249c54 100644
--- a/src/ir/phpcore/phpcore.go
+++ b/src/ir/phpcore/phpcore.go
@@ -9,7 +9,7 @@ func ResolveAlias(function ir.Node) ir.Node {
 	if !ok {
 		return function
 	}
-	alias, ok := funcAliases[nm.Value]
+	alias, ok := FuncAliases[nm.Value]
 	if ok {
 		return alias
 	}
@@ -17,41 +17,138 @@ func ResolveAlias(function ir.Node) ir.Node {
 }
 
 func ResolveAliasName(n *ir.Name) *ir.Name {
-	alias, ok := funcAliases[n.Value]
+	alias, ok := FuncAliases[n.Value]
 	if ok {
 		return alias
 	}
 	return n
 }
 
-var funcAliases = map[string]*ir.Name{
+var FuncAliases = map[string]*ir.Name{
 	// See https://www.php.net/manual/ru/aliases.php
 
-	`doubleval`: {Value: `floatval`},
-
-	`ini_alter`:  {Value: `ini_set`},
-	`is_integer`: {Value: `is_int`},
-	`is_long`:    {Value: `is_int`},
-	`is_real`:    {Value: `is_float`},
-	`is_double`:  {Value: `is_float`},
-
-	`join`:       {Value: `implode`},
-	`chop`:       {Value: `rtrim`},
-	`strchr`:     {Value: `strstr`},
-	`pos`:        {Value: `current`},
-	`key_exists`: {Value: `array_key_exists`},
-	`sizeof`:     {Value: `count`},
-
-	`checkdnsrr`: {Value: `dns_check_record`},
-	`getmxrr`:    {Value: `dns_get_mx`},
-
-	`is_writeable`:         {Value: `is_writable`},
-	`diskfreespace`:        {Value: `disk_free_space`},
-	`close`:                {Value: `closedir`},
-	`fputs`:                {Value: `fwrite`},
-	`magic_quotes_runtime`: {Value: `set_magic_quotes_runtime`},
-	`show_source`:          {Value: `highlight_file`},
-
+	"chop":                    {Value: "rtrim"},
+	"close":                   {Value: "closedir"},
+	"com_get":                 {Value: "com_propget"},
+	"com_propset":             {Value: "com_propput"},
+	"com_set":                 {Value: "com_propput"},
+	"die":                     {Value: "exit"},
+	"diskfreespace":           {Value: "disk_free_space"},
+	"doubleval":               {Value: "floatval"},
+	"fputs":                   {Value: "fwrite"},
+	"gzputs":                  {Value: "gzwrite"},
+	"i18n_convert":            {Value: "mb_convert_encoding"},
+	"i18n_discover_encoding":  {Value: "mb_detect_encoding"},
+	"i18n_http_input":         {Value: "mb_http_input"},
+	"i18n_http_output":        {Value: "mb_http_output"},
+	"i18n_internal_encoding":  {Value: "mb_internal_encoding"},
+	"i18n_ja_jp_hantozen":     {Value: "mb_convert_kana"},
+	"i18n_mime_header_decode": {Value: "mb_decode_mimeheader"},
+	"i18n_mime_header_encode": {Value: "mb_encode_mimeheader"},
+	"imap_create":             {Value: "imap_createmailbox"},
+	"imap_fetchtext":          {Value: "imap_body"},
+	"imap_getmailboxes":       {Value: "imap_list_full"},
+	"imap_getsubscribed":      {Value: "imap_lsub_full"},
+	"imap_header":             {Value: "imap_headerinfo"},
+	"imap_listmailbox":        {Value: "imap_list"},
+	"imap_listsubscribed":     {Value: "imap_lsub"},
+	"imap_rename":             {Value: "imap_renamemailbox"},
+	"imap_scan":               {Value: "imap_listscan"},
+	"imap_scanmailbox":        {Value: "imap_listscan"},
+	"ini_alter":               {Value: "ini_set"},
+	"is_double":               {Value: "is_float"},
+	"is_integer":              {Value: "is_int"},
+	"is_long":                 {Value: "is_int"},
+	"is_real":                 {Value: "is_float"},
+	"is_writeable":            {Value: "is_writable"},
+	"join":                    {Value: "implode"},
+	"key_exists":              {Value: "array_key_exists"},
+	"ldap_close":              {Value: "ldap_unbind"},
+	"mbstrcut":                {Value: "mb_strcut"},
+	"mbstrlen":                {Value: "mb_strlen"},
+	"mbstrpos":                {Value: "mb_strpos"},
+	"mbstrrpos":               {Value: "mb_strrpos"},
+	"mbsubstr":                {Value: "mb_substr"},
+	"mysql":                   {Value: "mysql_db_query"},
+	"mysql_createdb":          {Value: "mysql_create_db"},
+	"mysql_db_name":           {Value: "mysql_result"},
+	"mysql_dbname":            {Value: "mysql_result"},
+	"mysql_dropdb":            {Value: "mysql_drop_db"},
+	"mysql_fieldflags":        {Value: "mysql_field_flags"},
+	"mysql_fieldlen":          {Value: "mysql_field_len"},
+	"mysql_fieldname":         {Value: "mysql_field_name"},
+	"mysql_fieldtable":        {Value: "mysql_field_table"},
+	"mysql_fieldtype":         {Value: "mysql_field_type"},
+	"mysql_freeresult":        {Value: "mysql_free_result"},
+	"mysql_listdbs":           {Value: "mysql_list_dbs"},
+	"mysql_listfields":        {Value: "mysql_list_fields"},
+	"mysql_listtables":        {Value: "mysql_list_tables"},
+	"mysql_numfields":         {Value: "mysql_num_fields"},
+	"mysql_numrows":           {Value: "mysql_num_rows"},
+	"mysql_selectdb":          {Value: "mysql_select_db"},
+	"mysql_tablename":         {Value: "mysql_result"},
+	"ociassignelem":           {Value: "OCICollection::assignElem"},
+	"ocibindbyname":           {Value: "oci_bind_by_name"},
+	"ocicancel":               {Value: "oci_cancel"},
+	"ocicloselob":             {Value: "OCILob::close"},
+	"ocicollappend":           {Value: "OCICollection::append"},
+	"ocicollassign":           {Value: "OCICollection::assign"},
+	"ocicollmax":              {Value: "OCICollection::max"},
+	"ocicollsize":             {Value: "OCICollection::size"},
+	"ocicolltrim":             {Value: "OCICollection::trim"},
+	"ocicolumnisnull":         {Value: "oci_field_is_null"},
+	"ocicolumnname":           {Value: "oci_field_name"},
+	"ocicolumnprecision":      {Value: "oci_field_precision"},
+	"ocicolumnscale":          {Value: "oci_field_scale"},
+	"ocicolumnsize":           {Value: "oci_field_size"},
+	"ocicolumntype":           {Value: "oci_field_type"},
+	"ocicolumntyperaw":        {Value: "oci_field_type_raw"},
+	"ocicommit":               {Value: "oci_commit"},
+	"ocidefinebyname":         {Value: "oci_define_by_name"},
+	"ocierror":                {Value: "oci_error"},
+	"ociexecute":              {Value: "oci_execute"},
+	"ocifetch":                {Value: "oci_fetch"},
+	"ocifetchinto":            {Value: "oci_fetch_object"},
+	"ocifetchstatement":       {Value: "oci_fetch_all"},
+	"ocifreecollection":       {Value: "OCICollection::free"},
+	"ocifreecursor":           {Value: "oci_free_statement"},
+	"ocifreedesc":             {Value: "oci_free_descriptor"},
+	"ocifreestatement":        {Value: "oci_free_statement"},
+	"ocigetelem":              {Value: "OCICollection::getElem"},
+	"ociinternaldebug":        {Value: "oci_internal_debug"},
+	"ociloadlob":              {Value: "OCILob::load"},
+	"ocilogon":                {Value: "oci_connect"},
+	"ocinewcollection":        {Value: "oci_new_collection"},
+	"ocinewcursor":            {Value: "oci_new_cursor"},
+	"ocinewdescriptor":        {Value: "oci_new_descriptor"},
+	"ocinlogon":               {Value: "oci_new_connect"},
+	"ocinumcols":              {Value: "oci_num_fields"},
+	"ociparse":                {Value: "oci_parse"},
+	"ocipasswordchange":       {Value: "oci_password_change"},
+	"ociplogon":               {Value: "oci_pconnect"},
+	"ociresult":               {Value: "oci_result"},
+	"ocirollback":             {Value: "oci_rollback"},
+	"ocisavelob":              {Value: "OCILob::save"},
+	"ocisavelobfile":          {Value: "OCILob::import"},
+	"ociserverversion":        {Value: "oci_server_version"},
+	"ocisetprefetch":          {Value: "oci_set_prefetch"},
+	"ocistatementtype":        {Value: "oci_statement_type"},
+	"ociwritelobtofile":       {Value: "OCILob::export"},
+	"ociwritetemporarylob":    {Value: "OCILob::writeTemporary"},
+	"odbc_do":                 {Value: "odbc_exec"},
+	"odbc_field_precision":    {Value: "odbc_field_len"},
+	"pg_clientencoding":       {Value: "pg_client_encoding"},
+	"pg_setclientencoding":    {Value: "pg_set_client_encoding"},
+	"pos":                     {Value: "current"},
+	"recode":                  {Value: "recode_string"},
+	"show_source":             {Value: "highlight_file"},
+	"sizeof":                  {Value: "count"},
+	"snmpwalkoid":             {Value: "snmprealwalk"},
+	"strchr":                  {Value: "strstr"},
+	"xptr_new_context":        {Value: "xpath_new_context"},
+	`checkdnsrr`:              {Value: `dns_check_record`},
+	`getmxrr`:                 {Value: `dns_get_mx`},
+	`magic_quotes_runtime`:    {Value: `set_magic_quotes_runtime`},
 	`stream_register_wrapper`: {Value: `stream_wrapper_register`},
 	`set_file_buffer`:         {Value: `stream_set_write_buffer`},
 	`socket_set_blocking`:     {Value: `stream_set_blocking`},
diff --git a/src/linter/block_linter.go b/src/linter/block_linter.go
index 11e11b2..e4be55c 100644
--- a/src/linter/block_linter.go
+++ b/src/linter/block_linter.go
@@ -1040,9 +1040,15 @@ func (b *blockLinter) checkFunctionCall(e *ir.FunctionCallExpr) {
 	call := resolveFunctionCall(b.walker.ctx.sc, b.classParseState(), b.walker.ctx.customTypes, e)
 	fqName := call.funcName
 
+	var trimName = strings.TrimPrefix(fqName, `\`)
+	var phpMasterFunc = phpcore.FuncAliases[trimName]
+	if phpMasterFunc != nil {
+		b.report(e, LevelWarning, "phpAliases", "Use %s instead of '%s'", phpMasterFunc.Value, trimName)
+		b.walker.r.addQuickFix("phpAliases", b.quickfix.PhpAliasesReplace(e.Function.(*ir.Name), phpMasterFunc.Value))
+	}
+
 	if call.isClosure {
-		varName := strings.TrimPrefix(fqName, `\`)
-		b.walker.untrackVarName(varName)
+		b.walker.untrackVarName(trimName)
 	} else {
 		b.checkFunctionAvailability(e, &call)
 		b.walker.r.checker.CheckNameCase(e.Function, call.funcName, call.info.Name)
diff --git a/src/linter/quickfix.go b/src/linter/quickfix.go
index 5c9b3bf..2b03e14 100644
--- a/src/linter/quickfix.go
+++ b/src/linter/quickfix.go
@@ -48,6 +48,14 @@ func (g *QuickFixGenerator) NullForNotNullableProperty(prop *ir.PropertyStmt) qu
 	}
 }
 
+func (g *QuickFixGenerator) PhpAliasesReplace(prop *ir.Name, masterFunction string) quickfix.TextEdit {
+	return quickfix.TextEdit{
+		StartPos:    prop.Position.StartPos,
+		EndPos:      prop.Position.EndPos,
+		Replacement: masterFunction,
+	}
+}
+
 func (g *QuickFixGenerator) notExplicitNullableParam(param ir.Node) quickfix.TextEdit {
 	var pos *position.Position
 	var value string
diff --git a/src/linter/report.go b/src/linter/report.go
index 97ad85b..9ef7e1f 100644
--- a/src/linter/report.go
+++ b/src/linter/report.go
@@ -53,6 +53,15 @@ return -9223372036854775808;`,
 			After: `return PHP_INT_MIN;`,
 		},
 
+		{
+			Name:     "phpAliases",
+			Default:  true,
+			Quickfix: true,
+			Comment:  `Report php aliases functions.`,
+			Before:   `join("", []);`,
+			After:    `implode("", []);`,
+		},
+
 		{
 			Name:     "discardExpr",
 			Default:  true,


Test patch:
diff --git a/src/tests/checkers/basic_test.go b/src/tests/checkers/basic_test.go
index 3594f26..d4dad7c 100644
--- a/src/tests/checkers/basic_test.go
+++ b/src/tests/checkers/basic_test.go
@@ -2146,6 +2146,7 @@ function f() {
 	test.Expect = []string{
 		`Use float cast instead of real`,
 		`Use is_float function instead of is_real`,
+		`Use is_float instead of 'is_real`,
 	}
 	test.RunAndMatch()
 }
diff --git a/src/tests/checkers/php_aliases_test.go b/src/tests/checkers/php_aliases_test.go
new file mode 100644
index 0000000..6fba1a8
--- /dev/null
+++ b/src/tests/checkers/php_aliases_test.go
@@ -0,0 +1,43 @@
+package checkers
+
+import (
+	"testing"
+
+	"github.com/VKCOM/noverify/src/linttest"
+)
+
+func TestPhpAliases(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+declare(strict_types = "1")
+$_ = join("", []);
+`)
+	test.Expect = []string{
+		`Call to undefined function join`,
+		`Use implode instead of 'join'`,
+	}
+	test.RunAndMatch()
+}
+
+func TestPhpAliasesFunctionCall(t *testing.T) {
+	test := linttest.NewSuite(t)
+	test.AddFile(`<?php
+declare(strict_types = "1")
+$_ = join("", []);
+
+function test($d){
+ocicollmax();
+}
+
+test(join("", []));
+`)
+	test.Expect = []string{
+		`Use OCICollection::max instead of 'ocicollmax'`,
+		`Call to undefined function ocicollmax`,
+		`Use implode instead of 'join'`,
+		`Call to undefined function join`,
+		`Use implode instead of 'join'`,
+		`Call to undefined function join`,
+	}
+	test.RunAndMatch()
+}
diff --git a/src/tests/golden/golden_test.go b/src/tests/golden/golden_test.go
index be50020..88657c9 100644
--- a/src/tests/golden/golden_test.go
+++ b/src/tests/golden/golden_test.go
@@ -39,6 +39,7 @@ func TestGolden(t *testing.T) {
 			Name: "embeddedrules",
 			Disable: []string{
 				"deadCode",
+				"phpAliases",
 			},
 			Deps: []string{
 				`stubs/phpstorm-stubs/pcre/pcre.php`,
@@ -95,7 +96,7 @@ func TestGolden(t *testing.T) {
 
 		{
 			Name:    "parsedown",
-			Disable: []string{"missingPhpdoc", "arraySyntax"},
+			Disable: []string{"missingPhpdoc", "arraySyntax", "phpAliases"},
 			Deps: []string{
 				`stubs/phpstorm-stubs/pcre/pcre.php`,
 				`stubs/phpstorm-stubs/mbstring/mbstring.php`,
@@ -104,7 +105,7 @@ func TestGolden(t *testing.T) {
 
 		{
 			Name:    "underscore",
-			Disable: []string{"missingPhpdoc"},
+			Disable: []string{"missingPhpdoc", "phpAliases"},
 			Deps: []string{
 				`stubs/phpstorm-stubs/pcre/pcre.php`,
 			},
diff --git a/src/tests/golden/testdata/quickfix/phpAliases.php b/src/tests/golden/testdata/quickfix/phpAliases.php
new file mode 100644
index 0000000..296b78c
--- /dev/null
+++ b/src/tests/golden/testdata/quickfix/phpAliases.php
@@ -0,0 +1,9 @@
+<?php
+declare(strict_types = "1")
+$_ = join("", []);
+
+function test($d){
+ocicollmax();
+}
+
+test(join("", []));
diff --git a/src/tests/golden/testdata/quickfix/phpAliases.php.fix.expected b/src/tests/golden/testdata/quickfix/phpAliases.php.fix.expected
new file mode 100644
index 0000000..7f6babb
--- /dev/null
+++ b/src/tests/golden/testdata/quickfix/phpAliases.php.fix.expected
@@ -0,0 +1,9 @@
+<?php
+declare(strict_types = "1")
+$_ = implode("", []);
+
+function test($d){
+OCICollection::max();
+}
+
+test(implode("", []));