Repository: mgechev/revive (go)

Issue:
Warn on filenames with non ASCII characters
As described [here](https://www.arp242.net/jia-tan-go.html), source files with names containing non-ASCII characters can be used to compromise a code base. 
Enforcing ASCII-only filenames helps preventing these kind of attacks. `revive` is the perfect place to implement such enforcement. 

There is already a `banned-characters` rule that helps mitigating some security risks, it seems a good candidate where to implement the checks on filenames.


Pull request:
adds filename-format rule
Closes #1090 by adding a new rule with a broad scope of enforcing filename format


Patch:
diff --git a/README.md b/README.md
index f3032c9..a661e31 100644
--- a/README.md
+++ b/README.md
@@ -548,6 +548,7 @@ List of all available rules. The rules ported from `golint` are left unchanged a
 | [`max-control-nesting`](./RULES_DESCRIPTIONS.md#max-control-nesting) |  int (defaults to 5)  | Sets restriction for maximum nesting of control structures. |    no    |  no   |
 | [`comments-density`](./RULES_DESCRIPTIONS.md#comments-density) |  int (defaults to 0)  | Enforces a minimum comment / code relation |    no    |  no   |
 | [`file-length-limit`](./RULES_DESCRIPTIONS.md#file-length-limit) | map (optional)| Enforces a maximum number of lines per file |    no    |  no   |
+| [`filename-format`](./RULES_DESCRIPTIONS.md#filename-format) | regular expression (optional) | Enforces the formatting of filenames |   no    |  no   |
 
 ## Configurable rules
 
diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md
index 3ef2acd..fcd4e68 100644
--- a/RULES_DESCRIPTIONS.md
+++ b/RULES_DESCRIPTIONS.md
@@ -39,6 +39,7 @@ List of all available rules.
   - [exported](#exported)
   - [file-header](#file-header)
   - [file-length-limit](#file-length-limit)
+  - [filename-format](#filename-format)
   - [flag-parameter](#flag-parameter)
   - [function-length](#function-length)
   - [function-result-limit](#function-result-limit)
@@ -519,6 +520,18 @@ Example:
   arguments = [{max=100,skipComments=true,skipBlankLines=true}]
 ```
 
+## filename-format
+_Description_: enforces conventions on source file names. By default, the rule enforces filenames of the form `^[_A-Za-z0-9][_A-Za-z0-9-]*.go$`: Optionally, the rule can be configured to enforce other forms.
+
+_Configuration_: (string) regular expression for source filenames.
+
+Example:
+
+```toml
+[rule.filename-format]
+    arguments=["^[_a-z][_a-z0-9]*.go$"]
+```
+
 ## flag-parameter
 
 _Description_: If a function controls the flow of another by passing it information on what to do, both functions are said to be [control-coupled](https://en.wikipedia.org/wiki/Coupling_(computer_programming)#Procedural_programming).
diff --git a/config/config.go b/config/config.go
index 5da64b6..16559f5 100644
--- a/config/config.go
+++ b/config/config.go
@@ -97,6 +97,7 @@ var allRules = append([]lint.Rule{
 	&rule.MaxControlNestingRule{},
 	&rule.CommentsDensityRule{},
 	&rule.FileLengthLimitRule{},
+	&rule.FilenameFormatRule{},
 }, defaultRules...)
 
 var allFormatters = []lint.Formatter{
diff --git a/rule/filename-format.go b/rule/filename-format.go
new file mode 100644
index 0000000..54e6a5b
--- /dev/null
+++ b/rule/filename-format.go
@@ -0,0 +1,87 @@
+package rule
+
+import (
+	"fmt"
+	"path/filepath"
+	"regexp"
+	"sync"
+	"unicode"
+
+	"github.com/mgechev/revive/lint"
+)
+
+// FilenameFormatRule lints source filenames according to a set of regular expressions given as arguments
+type FilenameFormatRule struct {
+	format *regexp.Regexp
+	sync.Mutex
+}
+
+// Apply applies the rule to the given file.
+func (r *FilenameFormatRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
+	r.configure(arguments)
+
+	filename := filepath.Base(file.Name)
+	if r.format.MatchString(filename) {
+		return nil
+	}
+
+	failureMsg := fmt.Sprintf("Filename %s is not of the format %s.%s", filename, r.format.String(), r.getMsgForNonAsciiChars(filename))
+	return []lint.Failure{{
+		Confidence: 1,
+		Failure:    failureMsg,
+		RuleName:   r.Name(),
+		Node:       file.AST.Name,
+	}}
+}
+
+func (r *FilenameFormatRule) getMsgForNonAsciiChars(str string) string {
+	result := ""
+	for _, c := range str {
+		if c <= unicode.MaxASCII {
+			continue
+		}
+
+		result += fmt.Sprintf(" Non ASCII character %c (%U) found.", c, c)
+	}
+
+	return result
+}
+
+// Name returns the rule name.
+func (*FilenameFormatRule) Name() string {
+	return "filename-format"
+}
+
+var defaultFormat = regexp.MustCompile("^[_A-Za-z0-9][_A-Za-z0-9-]*.go$")
+
+func (r *FilenameFormatRule) configure(arguments lint.Arguments) {
+	r.Lock()
+	defer r.Unlock()
+
+	if r.format != nil {
+		return
+	}
+
+	argsCount := len(arguments)
+	if argsCount == 0 {
+		r.format = defaultFormat
+		return
+	}
+
+	if argsCount > 1 {
+		panic(fmt.Sprintf("rule %q expects only one argument, got %d %v", r.Name(), argsCount, arguments))
+	}
+
+	arg := arguments[0]
+	str, ok := arg.(string)
+	if !ok {
+		panic(fmt.Sprintf("rule %q expects a string argument, got %v of type %T", r.Name(), arg, arg))
+	}
+
+	format, err := regexp.Compile(str)
+	if err != nil {
+		panic(fmt.Sprintf("rule %q expects a valid regexp argument, got %v for %s", r.Name(), err, arg))
+	}
+
+	r.format = format
+}
diff --git "a/testdata/filenam\320\265-with-non-ascii-char.go" "b/testdata/filenam\320\265-with-non-ascii-char.go"
new file mode 100644
index 0000000..a4c665e
--- /dev/null
+++ "b/testdata/filenam\320\265-with-non-ascii-char.go"
@@ -0,0 +1,3 @@
+package main
+
+// MATCH:1 /Filename filenamе-with-non-ascii-char.go is not of the format ^[_A-Za-z0-9][_A-Za-z0-9-]*.go$. Non ASCII character е (U+0435) found./


Test patch:
diff --git a/test/filename-format_test.go b/test/filename-format_test.go
new file mode 100644
index 0000000..2f0ef76
--- /dev/null
+++ b/test/filename-format_test.go
@@ -0,0 +1,16 @@
+package test
+
+import (
+	"testing"
+
+	"github.com/mgechev/revive/lint"
+	"github.com/mgechev/revive/rule"
+)
+
+func TestLintFilenameFormat(t *testing.T) {
+	testRule(t, "filename-ok-default", &rule.FilenameFormatRule{}, &lint.RuleConfig{})
+
+	testRule(t, "filenamе-with-non-ascii-char", &rule.FilenameFormatRule{}, &lint.RuleConfig{})
+
+	testRule(t, "filename_with_underscores", &rule.FilenameFormatRule{}, &lint.RuleConfig{Arguments: []any{"^[A-Za-z][A-Za-z0-9]*.go$"}})
+}
diff --git a/testdata/filename-ok-default.go b/testdata/filename-ok-default.go
new file mode 100644
index 0000000..06ab7d0
--- /dev/null
+++ b/testdata/filename-ok-default.go
@@ -0,0 +1,1 @@
+package main
diff --git a/testdata/filename_with_underscores.go b/testdata/filename_with_underscores.go
new file mode 100644
index 0000000..cbdc6b5
--- /dev/null
+++ b/testdata/filename_with_underscores.go
@@ -0,0 +1,3 @@
+package main
+
+// MATCH:1 /Filename filename_with_underscores.go is not of the format ^[A-Za-z][A-Za-z0-9]*.go$./