Repository: mgechev/revive (go)

Issue:
Supporting multiple expressions for import-alias-naming
In my initial PR about adding [`import-alias-naming`](#881 ) we agreed to have only one expression. However, I'm now not able to easily solve the following limitation:

- allow all alphanumeric aliases with optional digits and have a length at least 1 symbol (i.e. `^[a-z][a-z0-9]*`)
- disallow `^v\d+$` aliases (e.g. `v1`)
- disallow `^v\d+alpha\d+$` aliases (e.g. `v1alpha1`)

In my PR I assumed that golang supports negative lookahead. Unfortunately, my assumption was wrong (as well as there's no support for positive lookahead, also in regex there's no general support for logical `AND`). Do you think, we can revise our decision and allow multiple rules that will be combined by logical `AND`? @chavacava 

I can submit a PR with an update (it will be backwards compatible).

Pull request description:
Add denyRegex support for import-alias-naming rule
This PR provides an extension to the existing `import-alias-naming` rule. While it preserves the backwards compatible configuration, with which it is possible to define an _allow_ regex pattern for import aliases, this PR brings a way to configure any or both pattern types: `allowRegex` and/or `denyRegex`. This brings two types of configuration:

1. The original one, allow pattern:
    ```toml
    # the original one, allow pattern
    [rule.import-alias-naming]
      arguments = ["^[a-z][a-z0-9]{0,}$"]
    ```
2. The new one, both allow and deny patterns:
    ```toml
    [rule.import-alias-naming]
      arguments = [ { allowRegex = "^[a-z][a-z0-9]{0,}$", denyRegex = '^v\d+$' } ]
    ```



Closes #924.

Patch:
diff --git a/README.md b/README.md
index b628652..8769df8 100644
--- a/README.md
+++ b/README.md
@@ -534,7 +534,7 @@ List of all available rules. The rules ported from `golint` are left unchanged a
 | [`datarace`](./RULES_DESCRIPTIONS.md#datarace)          |  n/a   |  Spots potential dataraces |    no    |  no   |
 | [`comment-spacings`](./RULES_DESCRIPTIONS.md#comment-spacings)          |  []string   |  Warns on malformed comments |    no    |  no   |
 | [`redundant-import-alias`](./RULES_DESCRIPTIONS.md#redundant-import-alias)          |  n/a   |  Warns on import aliases matching the imported package name |    no    |  no   |
-| [`import-alias-naming`](./RULES_DESCRIPTIONS.md#import-alias-naming)         | string (defaults to ^[a-z][a-z0-9]{0,}$) | Conventions around the naming of import aliases.                              |    no    |  no   |
+| [`import-alias-naming`](./RULES_DESCRIPTIONS.md#import-alias-naming)         | string or map[string]string (defaults to allow regex pattern ^[a-z][a-z0-9]{0,}$) | Conventions around the naming of import aliases.                              |    no    |  no   |
 | [`enforce-map-style`](./RULES_DESCRIPTIONS.md#enforce-map-style) |  string (defaults to "any")  |  Enforces consistent usage of `make(map[type]type)` or `map[type]type{}` for map initialization. Does not affect `make(map[type]type, size)` constructions. |    no    |  no   |
 | [`enforce-slice-style`](./RULES_DESCRIPTIONS.md#enforce-slice-style) |  string (defaults to "any")  |  Enforces consistent usage of `make([]type, 0)` or `[]type{}` for slice initialization. Does not affect `make(map[type]type, non_zero_len, or_non_zero_cap)` constructions. |    no    |  no   |
 
diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md
index f4aeb48..dad372b 100644
--- a/RULES_DESCRIPTIONS.md
+++ b/RULES_DESCRIPTIONS.md
@@ -489,13 +489,27 @@ _Description_: Aligns with Go's naming conventions, as outlined in the official
 the principles of good package naming. Users can follow these guidelines by default or define a custom regex rule.
 Importantly, aliases with underscores ("_") are always allowed.
 
-_Configuration_: (string) regular expression to match the aliases (default: ^[a-z][a-z0-9]{0,}$)
+_Configuration_ (1): (string) as plain string accepts allow regexp pattern for aliases (default: ^[a-z][a-z0-9]{0,}$),
+_Configuration_ (2): (map[string]string) as a map accepts two values:
+* for a key "allowRegex" accepts allow regexp pattern
+* for a key "denyRegex deny regexp pattern
 
-Example:
+_Note_: If both `allowRegex` and `denyRegex` are provided, the alias must comply with both of them.
+If none are given (i.e. an empty map), the default value ^[a-z][a-z0-9]{0,}$ for allowRegex is used.
+Unknown keys will result in an error.
+
+Example (1):
+
+```toml
+[rule.import-alias-naming]
+  arguments = ["^[a-z][a-z0-9]{0,}$"]
+```
+
+Example (2):
 
 ```toml
 [rule.import-alias-naming]
-  arguments =["^[a-z][a-z0-9]{0,}$"]
+  arguments = [ { allowRegex = "^[a-z][a-z0-9]{0,}$", denyRegex = '^v\d+$' } ]
 ```
 
 ## import-shadowing
diff --git a/rule/import-alias-naming.go b/rule/import-alias-naming.go
index 292392b..0c6513d 100644
--- a/rule/import-alias-naming.go
+++ b/rule/import-alias-naming.go
@@ -10,14 +10,15 @@ import (
 
 // ImportAliasNamingRule lints import alias naming.
 type ImportAliasNamingRule struct {
-	configured       bool
-	namingRuleRegexp *regexp.Regexp
+	configured  bool
+	allowRegexp *regexp.Regexp
+	denyRegexp  *regexp.Regexp
 	sync.Mutex
 }
 
-const defaultNamingRule = "^[a-z][a-z0-9]{0,}$"
+const defaultImportAliasNamingAllowRule = "^[a-z][a-z0-9]{0,}$"
 
-var defaultNamingRuleRegexp = regexp.MustCompile(defaultNamingRule)
+var defaultImportAliasNamingAllowRegexp = regexp.MustCompile(defaultImportAliasNamingAllowRule)
 
 func (r *ImportAliasNamingRule) configure(arguments lint.Arguments) {
 	r.Lock()
@@ -26,20 +27,31 @@ func (r *ImportAliasNamingRule) configure(arguments lint.Arguments) {
 		return
 	}
 
-	if len(arguments) < 1 {
-		r.namingRuleRegexp = defaultNamingRuleRegexp
+	if len(arguments) == 0 {
+		r.allowRegexp = defaultImportAliasNamingAllowRegexp
 		return
 	}
 
-	namingRule, ok := arguments[0].(string) // Alt. non panicking version
-	if !ok {
-		panic(fmt.Sprintf("Invalid argument '%v' for 'import-alias-naming' rule. Expecting string, got %T", arguments[0], arguments[0]))
+	switch namingRule := arguments[0].(type) {
+	case string:
+		r.setAllowRule(namingRule)
+	case map[string]any: // expecting map[string]string
+		for k, v := range namingRule {
+			switch k {
+			case "allowRegex":
+				r.setAllowRule(v)
+			case "denyRegex":
+				r.setDenyRule(v)
+			default:
+				panic(fmt.Sprintf("Invalid map key for 'import-alias-naming' rule. Expecting 'allowRegex' or 'denyRegex', got %v", k))
+			}
+		}
+	default:
+		panic(fmt.Sprintf("Invalid argument '%v' for 'import-alias-naming' rule. Expecting string or map[string]string, got %T", arguments[0], arguments[0]))
 	}
 
-	var err error
-	r.namingRuleRegexp, err = regexp.Compile(namingRule)
-	if err != nil {
-		panic(fmt.Sprintf("Invalid argument to the import-alias-naming rule. Expecting %q to be a valid regular expression, got: %v", namingRule, err))
+	if r.allowRegexp == nil && r.denyRegexp == nil {
+		r.allowRegexp = defaultImportAliasNamingAllowRegexp
 	}
 }
 
@@ -60,10 +72,19 @@ func (r *ImportAliasNamingRule) Apply(file *lint.File, arguments lint.Arguments)
 			continue
 		}
 
-		if !r.namingRuleRegexp.MatchString(alias.Name) {
+		if r.allowRegexp != nil && !r.allowRegexp.MatchString(alias.Name) {
+			failures = append(failures, lint.Failure{
+				Confidence: 1,
+				Failure:    fmt.Sprintf("import name (%s) must match the regular expression: %s", alias.Name, r.allowRegexp.String()),
+				Node:       alias,
+				Category:   "imports",
+			})
+		}
+
+		if r.denyRegexp != nil && r.denyRegexp.MatchString(alias.Name) {
 			failures = append(failures, lint.Failure{
 				Confidence: 1,
-				Failure:    fmt.Sprintf("import name (%s) must match the regular expression: %s", alias.Name, r.namingRuleRegexp.String()),
+				Failure:    fmt.Sprintf("import name (%s) must NOT match the regular expression: %s", alias.Name, r.denyRegexp.String()),
 				Node:       alias,
 				Category:   "imports",
 			})
@@ -77,3 +98,29 @@ func (r *ImportAliasNamingRule) Apply(file *lint.File, arguments lint.Arguments)
 func (*ImportAliasNamingRule) Name() string {
 	return "import-alias-naming"
 }
+
+func (r *ImportAliasNamingRule) setAllowRule(value any) {
+	namingRule, ok := value.(string)
+	if !ok {
+		panic(fmt.Sprintf("Invalid argument '%v' for import-alias-naming allowRegexp rule. Expecting string, got %T", value, value))
+	}
+
+	namingRuleRegexp, err := regexp.Compile(namingRule)
+	if err != nil {
+		panic(fmt.Sprintf("Invalid argument to the import-alias-naming allowRegexp rule. Expecting %q to be a valid regular expression, got: %v", namingRule, err))
+	}
+	r.allowRegexp = namingRuleRegexp
+}
+
+func (r *ImportAliasNamingRule) setDenyRule(value any) {
+	namingRule, ok := value.(string)
+	if !ok {
+		panic(fmt.Sprintf("Invalid argument '%v' for import-alias-naming denyRegexp rule. Expecting string, got %T", value, value))
+	}
+
+	namingRuleRegexp, err := regexp.Compile(namingRule)
+	if err != nil {
+		panic(fmt.Sprintf("Invalid argument to the import-alias-naming denyRegexp rule. Expecting %q to be a valid regular expression, got: %v", namingRule, err))
+	}
+	r.denyRegexp = namingRuleRegexp
+}


Test patch:
diff --git a/test/import-alias-naming_test.go b/test/import-alias-naming_test.go
index e772a09..a4782c2 100644
--- a/test/import-alias-naming_test.go
+++ b/test/import-alias-naming_test.go
@@ -9,7 +9,36 @@ import (
 
 func TestImportAliasNaming(t *testing.T) {
 	testRule(t, "import-alias-naming", &rule.ImportAliasNamingRule{})
+	testRule(t, "import-alias-naming", &rule.ImportAliasNamingRule{}, &lint.RuleConfig{
+		Arguments: lint.Arguments{
+			map[string]any{},
+		},
+	})
+}
+
+func TestImportAliasNaming_CustomConfig(t *testing.T) {
 	testRule(t, "import-alias-naming-custom-config", &rule.ImportAliasNamingRule{}, &lint.RuleConfig{
 		Arguments: []any{`^[a-z]+$`},
 	})
 }
+
+func TestImportAliasNaming_CustomConfigWithMultipleRules(t *testing.T) {
+	testRule(t, "import-alias-naming-custom-config-with-multiple-values", &rule.ImportAliasNamingRule{}, &lint.RuleConfig{
+		Arguments: []any{
+			map[string]any{
+				"allowRegex": `^[a-z][a-z0-9]*$`,
+				"denyRegex":  `^((v\d+)|(v\d+alpha\d+))$`,
+			},
+		},
+	})
+}
+
+func TestImportAliasNaming_CustomConfigWithOnlyDeny(t *testing.T) {
+	testRule(t, "import-alias-naming-custom-config-with-only-deny", &rule.ImportAliasNamingRule{}, &lint.RuleConfig{
+		Arguments: []any{
+			map[string]any{
+				"denyRegex": `^((v\d+)|(v\d+alpha\d+))$`,
+			},
+		},
+	})
+}
diff --git a/testdata/import-alias-naming-custom-config-with-multiple-values.go b/testdata/import-alias-naming-custom-config-with-multiple-values.go
new file mode 100644
index 0000000..cbfb20b
--- /dev/null
+++ b/testdata/import-alias-naming-custom-config-with-multiple-values.go
@@ -0,0 +1,17 @@
+package fixtures
+
+import (
+	_ "strings"
+	bar_foo "strings"  // MATCH /import name (bar_foo) must match the regular expression: ^[a-z][a-z0-9]*$/
+	fooBAR "strings"   // MATCH /import name (fooBAR) must match the regular expression: ^[a-z][a-z0-9]*$/
+	v1 "strings"       // MATCH /import name (v1) must NOT match the regular expression: ^((v\d+)|(v\d+alpha\d+))$/
+	v1alpha1 "strings" // MATCH /import name (v1alpha1) must NOT match the regular expression: ^((v\d+)|(v\d+alpha\d+))$/
+	magical "magic/hat"
+)
+
+func somefunc() {
+	fooBAR.Clone("")
+	bar_foo.Clone("")
+	v1.Clone("")
+	magical.Clone("")
+}
diff --git a/testdata/import-alias-naming-custom-config-with-only-deny.go b/testdata/import-alias-naming-custom-config-with-only-deny.go
new file mode 100644
index 0000000..771ef55
--- /dev/null
+++ b/testdata/import-alias-naming-custom-config-with-only-deny.go
@@ -0,0 +1,17 @@
+package fixtures
+
+import (
+	_ "strings"
+	bar_foo "strings"
+	fooBAR "strings"
+	v1 "strings"       // MATCH /import name (v1) must NOT match the regular expression: ^((v\d+)|(v\d+alpha\d+))$/
+	v1alpha1 "strings" // MATCH /import name (v1alpha1) must NOT match the regular expression: ^((v\d+)|(v\d+alpha\d+))$/
+	magical "magic/hat"
+)
+
+func somefunc() {
+	fooBAR.Clone("")
+	bar_foo.Clone("")
+	v1.Clone("")
+	magical.Clone("")
+}