Repository: mgechev/revive (go)

Issue:
[rule.add-constant] add ''ignoreFuncs' to exclude constants in functions
**Is your feature request related to a problem? Please describe.**
I want to add 'ignoreFuncs' in configuration
to have a possibility to exclude constants in functions

```
[rule.add-constant]
  arguments = [{'ignoreFuncs="println,os.Chmod"}]
```

**Describe the solution you'd like**
I want to create PR with changes, which add this feature


Pull request description:
fix #751 [rule.add-constant] add ignoreFuncs to exclude constants in …
Closes #751 

Patch:
diff --git a/README.md b/README.md
index b8c7610..e2b97b1 100644
--- a/README.md
+++ b/README.md
@@ -66,8 +66,10 @@ Here's how `revive` is different from `golint`:
 - [revive](#revive)
   - [Installation](#installation)
   - [Usage](#usage)
+    - [Docker](#docker)
     - [Bazel](#bazel)
     - [Text Editors](#text-editors)
+    - [GitHub Actions](#github-actions)
     - [Continuous Integration](#continuous-integration)
     - [Linter Aggregators](#linter-aggregators)
       - [golangci-lint](#golangci-lint)
@@ -89,7 +91,7 @@ Here's how `revive` is different from `golint`:
     - [Unix](#unix)
     - [SARIF](#sarif)
   - [Extensibility](#extensibility)
-    - [Custom Rule](#custom-rule)
+    - [Custom Rule](#writing-a-custom-rule)
       - [Example](#example)
     - [Custom Formatter](#custom-formatter)
   - [Speed Comparison](#speed-comparison)
diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md
index ece29e9..56ad711 100644
--- a/RULES_DESCRIPTIONS.md
+++ b/RULES_DESCRIPTIONS.md
@@ -85,12 +85,13 @@ _Configuration_:
 * `allowStr`: (string) comma-separated list of allowed string literals
 * `allowInts`: (string) comma-separated list of allowed integers
 * `allowFloats`: (string) comma-separated list of allowed floats
+* `ignoreFuncs`: (string) comma-separated list of function names regexp patterns to exclude
 
 Example:
 
 ```toml
 [rule.add-constant]
-  arguments = [{maxLitCount = "3",allowStrs ="\"\"",allowInts="0,1,2",allowFloats="0.0,0.,1.0,1.,2.0,2."}]
+  arguments = [{maxLitCount = "3",allowStrs ="\"\"",allowInts="0,1,2",allowFloats="0.0,0.,1.0,1.,2.0,2.","ignoreFuncs": "os.*,fmt.Println,make"}]
 ```
 
 ## argument-limit
diff --git a/rule/add-constant.go b/rule/add-constant.go
index 414be38..36a7003 100644
--- a/rule/add-constant.go
+++ b/rule/add-constant.go
@@ -3,6 +3,7 @@ package rule
 import (
 	"fmt"
 	"go/ast"
+	"regexp"
 	"strconv"
 	"strings"
 	"sync"
@@ -32,8 +33,9 @@ func (wl whiteList) add(kind, list string) {
 
 // AddConstantRule lints unused params in functions.
 type AddConstantRule struct {
-	whiteList   whiteList
-	strLitLimit int
+	whiteList       whiteList
+	ignoreFunctions []*regexp.Regexp
+	strLitLimit     int
 	sync.Mutex
 }
 
@@ -47,7 +49,13 @@ func (r *AddConstantRule) Apply(file *lint.File, arguments lint.Arguments) []lin
 		failures = append(failures, failure)
 	}
 
-	w := lintAddConstantRule{onFailure: onFailure, strLits: make(map[string]int), strLitLimit: r.strLitLimit, whiteLst: r.whiteList}
+	w := lintAddConstantRule{
+		onFailure:       onFailure,
+		strLits:         make(map[string]int),
+		strLitLimit:     r.strLitLimit,
+		whiteLst:        r.whiteList,
+		ignoreFunctions: r.ignoreFunctions,
+	}
 
 	ast.Walk(w, file.AST)
 
@@ -60,28 +68,76 @@ func (*AddConstantRule) Name() string {
 }
 
 type lintAddConstantRule struct {
-	onFailure   func(lint.Failure)
-	strLits     map[string]int
-	strLitLimit int
-	whiteLst    whiteList
+	onFailure       func(lint.Failure)
+	strLits         map[string]int
+	strLitLimit     int
+	whiteLst        whiteList
+	ignoreFunctions []*regexp.Regexp
 }
 
 func (w lintAddConstantRule) Visit(node ast.Node) ast.Visitor {
 	switch n := node.(type) {
+	case *ast.CallExpr:
+		w.checkFunc(n)
+		return nil
 	case *ast.GenDecl:
 		return nil // skip declarations
 	case *ast.BasicLit:
-		switch kind := n.Kind.String(); kind {
-		case kindFLOAT, kindINT:
-			w.checkNumLit(kind, n)
-		case kindSTRING:
-			w.checkStrLit(n)
-		}
+		w.checkLit(n)
 	}
 
 	return w
 }
 
+func (w lintAddConstantRule) checkFunc(expr *ast.CallExpr) {
+	fName := w.getFuncName(expr)
+
+	for _, arg := range expr.Args {
+		switch t := arg.(type) {
+		case *ast.CallExpr:
+			w.checkFunc(t)
+		case *ast.BasicLit:
+			if w.isIgnoredFunc(fName) {
+				continue
+			}
+			w.checkLit(t)
+		}
+	}
+}
+
+func (w lintAddConstantRule) getFuncName(expr *ast.CallExpr) string {
+	switch f := expr.Fun.(type) {
+	case *ast.SelectorExpr:
+		switch prefix := f.X.(type) {
+		case *ast.Ident:
+			return prefix.Name + "." + f.Sel.Name
+		}
+	case *ast.Ident:
+		return f.Name
+	}
+
+	return ""
+}
+
+func (w lintAddConstantRule) checkLit(n *ast.BasicLit) {
+	switch kind := n.Kind.String(); kind {
+	case kindFLOAT, kindINT:
+		w.checkNumLit(kind, n)
+	case kindSTRING:
+		w.checkStrLit(n)
+	}
+}
+
+func (w lintAddConstantRule) isIgnoredFunc(fName string) bool {
+	for _, pattern := range w.ignoreFunctions {
+		if pattern.MatchString(fName) {
+			return true
+		}
+	}
+
+	return false
+}
+
 func (w lintAddConstantRule) checkStrLit(n *ast.BasicLit) {
 	if w.whiteLst[kindSTRING][n.Value] {
 		return
@@ -158,6 +214,25 @@ func (r *AddConstantRule) configure(arguments lint.Arguments) {
 						panic(fmt.Sprintf("Invalid argument to the add-constant rule, expecting string representation of an integer. Got '%v'", v))
 					}
 					r.strLitLimit = limit
+				case "ignoreFuncs":
+					excludes, ok := v.(string)
+					if !ok {
+						panic(fmt.Sprintf("Invalid argument to the ignoreFuncs parameter of add-constant rule, string expected. Got '%v' (%T)", v, v))
+					}
+
+					for _, exclude := range strings.Split(excludes, ",") {
+						exclude = strings.Trim(exclude, " ")
+						if exclude == "" {
+							panic("Invalid argument to the ignoreFuncs parameter of add-constant rule, expected regular expression must not be empty.")
+						}
+
+						exp, err := regexp.Compile(exclude)
+						if err != nil {
+							panic(fmt.Sprintf("Invalid argument to the ignoreFuncs parameter of add-constant rule: regexp %q does not compile: %v", exclude, err))
+						}
+
+						r.ignoreFunctions = append(r.ignoreFunctions, exp)
+					}
 				}
 			}
 		}


Test patch:
diff --git a/test/add-constant_test.go b/test/add-constant_test.go
index e4e4aec..16eb173 100644
--- a/test/add-constant_test.go
+++ b/test/add-constant_test.go
@@ -13,6 +13,7 @@ func TestAddConstant(t *testing.T) {
 		"allowStrs":   "\"\"",
 		"allowInts":   "0,1,2",
 		"allowFloats": "0.0,1.0",
+		"ignoreFuncs": "os\\.(CreateFile|WriteFile|Chmod|FindProcess),.Println,ignoredFunc",
 	}}
 
 	testRule(t, "add-constant", &rule.AddConstantRule{}, &lint.RuleConfig{
diff --git a/testdata/add-constant.go b/testdata/add-constant.go
index d240f40..4017ccd 100644
--- a/testdata/add-constant.go
+++ b/testdata/add-constant.go
@@ -1,5 +1,10 @@
 package fixtures
 
+import (
+	"fmt"
+	"os"
+)
+
 func foo(a, b, c, d int) {
 	a = 1.0 // ignore
 	b = "ignore"
@@ -13,4 +18,34 @@ func foo(a, b, c, d int) {
 			println("lit")
 		}
 	}
+
+	println(0666)           // MATCH /avoid magic numbers like '0666', create a named constant for it/
+	os.Chmod("test", 0666)  // ignore
+	os.FindProcess(102100)  // ignore
+	fmt.Println("test", 12) // ignore
+	fmt.Printf("%d", 100)   // MATCH /avoid magic numbers like '100', create a named constant for it/
+	ignoredFunc(1000)       // ignore
+
+	println("The result of calling myFunc is: ", ignoredFunc(100))             // ignore
+	println("result is: ", ignoredFunc(notIgnoredFunc(ignoredFunc(100))))      // ignore
+	println("result of calling myFunc is: ", notIgnoredFunc(ignoredFunc(100))) // ignore
+
+	println("result myFunc is: ", notIgnoredFunc(100))           // MATCH /avoid magic numbers like '100', create a named constant for it/
+	println("The result is: ", ignoredFunc(notIgnoredFunc(100))) // MATCH /avoid magic numbers like '100', create a named constant for it/
+}
+
+func myPrintln(s string, num int) {
+
+}
+
+func not2ignoredFunc(num int) int {
+	return num
+}
+
+func ignoredFunc(num int) int {
+	return num
+}
+
+func notIgnoredFunc(num int) int {
+	return num
 }