Repository: mgechev/revive (go)

Issue:
Linter rule to check against import alias naming restrictions
**Is your feature request related to a problem? Please describe.**

Go gives some recommendations on [how packages should be named](https://go.dev/blog/package-names). This should also imply the rules for the aliases. I'd like to check that aliases for packages in our project don't include CAPS and underscores.

**Describe the solution you'd like**
I have two options:

* Regex mask that would fit any possible extra limitations that the project might want to have (for example completely forbid `v1` alias)
* Add 2 configuration options: `forbidCAPS` and `forbidUnderscores`.

The first one is more universal, the second one is simpler and more precise. We can also combine both options but then it might bring confusion.

NB: in any case we'll skip `_` import alias used for side effects.

**Describe alternatives you've considered**
I checked `importas` linter and it didn't have this feature. I submitted a feature request there, but I'm not sure if it fits the idea of `importas`.

**Additional context**
Example:

```go
package foo

import (
    fooBAR "foo/bar" // MATCH /import alias should not include capital letters/
    bar_foo "bar/foo" // MATCH /import alias should not include underscores/
    _ "far/boo" // this is fine
)
```

OR

```go
package foo

import (
    foo_BAR "foo/bar" // MATCH /import alias must much the regular expression: "^[a-z][a-z0-9]{0,}$"/
    _ "far/boo" // this is fine
)
```

Pull request:
feat: add support for import-alias-naming rule
Closes #880.

Patch:
diff --git a/README.md b/README.md
index 2bdb47e..daadbc1 100644
--- a/README.md
+++ b/README.md
@@ -533,6 +533,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   |
 
 
 ## Configurable rules
diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md
index 3d78110..4561e20 100644
--- a/RULES_DESCRIPTIONS.md
+++ b/RULES_DESCRIPTIONS.md
@@ -44,6 +44,7 @@ List of all available rules.
   - [indent-error-flow](#indent-error-flow)
   - [imports-blacklist](#imports-blacklist)
   - [import-shadowing](#import-shadowing)
+  - [import-alias-naming](#import-alias-naming)
   - [line-length-limit](#line-length-limit)
   - [max-public-structs](#max-public-structs)
   - [modifies-parameter](#modifies-parameter)
@@ -489,6 +490,22 @@ name of an imported package. This rule spots identifiers that shadow an import.
 
 _Configuration_: N/A
 
+## import-alias-naming
+
+_Description_: Aligns with Go's naming conventions, as outlined in the official
+[blog post](https://go.dev/blog/package-names). It enforces clear and lowercase import alias names, echoing
+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,}$)
+
+Example:
+
+```toml
+[rule.import-alias-naming]
+  arguments =["^[a-z][a-z0-9]{0,}$"]
+```
+
 ## line-length-limit
 
 _Description_: Warns in the presence of code lines longer than a configured maximum.
diff --git a/config/config.go b/config/config.go
index 7643d27..4385459 100644
--- a/config/config.go
+++ b/config/config.go
@@ -88,6 +88,7 @@ var allRules = append([]lint.Rule{
 	&rule.CommentSpacingsRule{},
 	&rule.IfReturnRule{},
 	&rule.RedundantImportAlias{},
+	&rule.ImportAliasNamingRule{},
 }, defaultRules...)
 
 var allFormatters = []lint.Formatter{
diff --git a/rule/import-alias-naming.go b/rule/import-alias-naming.go
new file mode 100644
index 0000000..292392b
--- /dev/null
+++ b/rule/import-alias-naming.go
@@ -0,0 +1,79 @@
+package rule
+
+import (
+	"fmt"
+	"regexp"
+	"sync"
+
+	"github.com/mgechev/revive/lint"
+)
+
+// ImportAliasNamingRule lints import alias naming.
+type ImportAliasNamingRule struct {
+	configured       bool
+	namingRuleRegexp *regexp.Regexp
+	sync.Mutex
+}
+
+const defaultNamingRule = "^[a-z][a-z0-9]{0,}$"
+
+var defaultNamingRuleRegexp = regexp.MustCompile(defaultNamingRule)
+
+func (r *ImportAliasNamingRule) configure(arguments lint.Arguments) {
+	r.Lock()
+	defer r.Unlock()
+	if r.configured {
+		return
+	}
+
+	if len(arguments) < 1 {
+		r.namingRuleRegexp = defaultNamingRuleRegexp
+		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]))
+	}
+
+	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))
+	}
+}
+
+// Apply applies the rule to given file.
+func (r *ImportAliasNamingRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
+	r.configure(arguments)
+
+	var failures []lint.Failure
+
+	for _, is := range file.AST.Imports {
+		path := is.Path
+		if path == nil {
+			continue
+		}
+
+		alias := is.Name
+		if alias == nil || alias.Name == "_" {
+			continue
+		}
+
+		if !r.namingRuleRegexp.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()),
+				Node:       alias,
+				Category:   "imports",
+			})
+		}
+	}
+
+	return failures
+}
+
+// Name returns the rule name.
+func (*ImportAliasNamingRule) Name() string {
+	return "import-alias-naming"
+}


Test patch:
diff --git a/test/import-alias-naming_test.go b/test/import-alias-naming_test.go
new file mode 100644
index 0000000..e772a09
--- /dev/null
+++ b/test/import-alias-naming_test.go
@@ -0,0 +1,15 @@
+package test
+
+import (
+	"testing"
+
+	"github.com/mgechev/revive/lint"
+	"github.com/mgechev/revive/rule"
+)
+
+func TestImportAliasNaming(t *testing.T) {
+	testRule(t, "import-alias-naming", &rule.ImportAliasNamingRule{})
+	testRule(t, "import-alias-naming-custom-config", &rule.ImportAliasNamingRule{}, &lint.RuleConfig{
+		Arguments: []any{`^[a-z]+$`},
+	})
+}
diff --git a/testdata/import-alias-naming-custom-config.go b/testdata/import-alias-naming-custom-config.go
new file mode 100644
index 0000000..7fc4d55
--- /dev/null
+++ b/testdata/import-alias-naming-custom-config.go
@@ -0,0 +1,16 @@
+package fixtures
+
+import (
+	_ "strings"
+	bar_foo "strings" // MATCH /import name (bar_foo) must match the regular expression: ^[a-z]+$/
+	fooBAR "strings"  // MATCH /import name (fooBAR) must match the regular expression: ^[a-z]+$/
+	v1 "strings"      // MATCH /import name (v1) must match the regular expression: ^[a-z]+$/
+	magical "magic/hat"
+)
+
+func somefunc() {
+	fooBAR.Clone("")
+	bar_foo.Clone("")
+	v1.Clone("")
+	magical.Clone("")
+}
diff --git a/testdata/import-alias-naming.go b/testdata/import-alias-naming.go
new file mode 100644
index 0000000..6e2c339
--- /dev/null
+++ b/testdata/import-alias-naming.go
@@ -0,0 +1,16 @@
+package fixtures
+
+import (
+	_ "strings"
+	bar_foo "strings" // MATCH /import name (bar_foo) must match the regular expression: ^[a-z][a-z0-9]{0,}$/
+	fooBAR "strings"  // MATCH /import name (fooBAR) must match the regular expression: ^[a-z][a-z0-9]{0,}$/
+	v1 "strings"
+	magical "magic/hat"
+)
+
+func somefunc() {
+	fooBAR.Clone("")
+	bar_foo.Clone("")
+	v1.Clone("")
+	magical.Clone("")
+}