Repository: mgechev/revive (go)

Issue:
`package-comments` ignores `//nolint` directives
**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. I updated revive `go install github.com/mgechev/revive@latest`
2. I run it with no flags or config file, on the following:

```
// myfile1.go
// Package mine does something
package mine

func doSomething() {}

// myfile2.go
//nolint:dupl
package mine

func doSomethingElse() {}
```

**Expected behavior**
No lint warnings, since the package does have a comment (in `myfile1.go`), and the other comment is a directive-type comment (specifically, it's for golangci-lint)

**Logs**

```
workspace/projects-scrap/package-comments-check via 🐹 v1.24.2 took 2s
❯ ./revive .
myfile2.go:1:1: package comment should be of the form "Package mine ..."
```

**Desktop (please complete the following information):**
 - OS: Ubuntu 20.04, via WSLv2
 - Version of Go: v1.24

**Additional context**

From what I can find, it seems like directives are meant to be ignored (e.g. https://github.com/mgechev/revive/pull/989).

The situation I'm dealing with is that the `dupl` linter is flagging code that includes the `package` line meaning we have to put our `nolint` directively immediately before that (including without any newlines between it) - while it looks like I can include a `//nolint:revive` in between, the danger is that could mistakenly lead to disabling `revive` for the whole file as that'd happen if the line is merged (`//nolint:dupl,revive`) or if the `dupl` linter line is removed (e.g. because the code is changed and then the linter complains the directive is not doing anything anymore)


Pull request:
fix(#1363): package-comments ignores //nolint directives
Closes #1363 

Root cause of the bug: when checking package comments, the rule doesn't skip directive-comments at the right place.

Patch:
diff --git a/README.md b/README.md
index c6dccbb..b55119b 100644
--- a/README.md
+++ b/README.md
@@ -530,6 +530,7 @@ List of all available rules. The rules ported from `golint` are left unchanged a
 | [`string-of-int`](./RULES_DESCRIPTIONS.md#string-of-int)          |  n/a   | Warns on suspicious casts from int to string            |    no    |  yes   |
 | [`struct-tag`](./RULES_DESCRIPTIONS.md#struct-tag)          |  []string   | Checks common struct tags like `json`, `xml`, `yaml`               |    no    |  no   |
 | [`superfluous-else`](./RULES_DESCRIPTIONS.md#superfluous-else)    |  []string   | Prevents redundant else statements (extends [`indent-error-flow`](./RULES_DESCRIPTIONS.md#indent-error-flow)) |    no    |  no   |
+| [`time-date`](./RULES_DESCRIPTIONS.md#time-date)         |  n/a   | Reports bad usage of `time.Date`.                 |   no    |  yes  |
 | [`time-equal`](./RULES_DESCRIPTIONS.md#time-equal)         |  n/a   | Suggests to use `time.Time.Equal` instead of `==` and `!=` for equality check time.                 |   no    |  yes  |
 | [`time-naming`](./RULES_DESCRIPTIONS.md#time-naming)         |  n/a   | Conventions around the naming of time variables.                 |   yes    |  yes  |
 | [`unchecked-type-assertions`](./RULES_DESCRIPTIONS.md#unchecked-type-assertions)         |  n/a   | Disallows type assertions without checking the result.                 |   no    |  yes  |
diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md
index 2ac0c1f..183b080 100644
--- a/RULES_DESCRIPTIONS.md
+++ b/RULES_DESCRIPTIONS.md
@@ -70,6 +70,7 @@ List of all available rules.
   - [string-of-int](#string-of-int)
   - [struct-tag](#struct-tag)
   - [superfluous-else](#superfluous-else)
+  - [time-date](#time-date)
   - [time-equal](#time-equal)
   - [time-naming](#time-naming)
   - [unchecked-type-assertion](#unchecked-type-assertion)
@@ -1013,6 +1014,55 @@ Examples:
   arguments = ["preserve-scope"]
 ```
 
+## time-date
+
+_Description_: Reports bad usage of `time.Date`.
+
+_Configuration_: N/A
+
+_Example_:
+
+Here the leading zeros are defining integers with octal notation
+
+```go
+var (
+  // here we can imagine zeroes were used for padding purpose
+  a = time.Date(2023, 1, 2, 3, 4, 0, 00000000, time.UTC) // 00000000 is octal and equals 0 in decimal
+  b = time.Date(2023, 1, 2, 3, 4, 0, 00000006, time.UTC) // 00000006 is octal and equals 6 in decimal
+  c = time.Date(2023, 1, 2, 3, 4, 0, 00123456, time.UTC) // 00123456 is octal and equals 42798 in decimal
+)
+```
+
+But here, what was expected 123456 or 42798 ? It's a source of bugs.
+
+So the rule will report this
+
+> octal notation with padding zeroes found: choose between 123456 and 42798 (decimal value of 123456 octal value)
+
+This rule also reports strange notations used with time.Date.
+
+Example:
+
+```go
+var _ = time.Date(
+		0x7e7,    // hexadecimal notation: use 2023 instead of 0x7e7/
+		0b1,      // binary notation: use 1 instead of 0b1/
+		0x_2,     // hexadecimal notation: use 2 instead of 0x_2/
+		1_3,      // alternative notation: use 13 instead of 1_3/
+		1e1,      // exponential notation: use 10 instead of 1e1/
+		0.,       // float literal: use 0 instead of 0./
+		0x1.Fp+6, // float literal: use 124 instead of 0x1.Fp+6/
+		time.UTC)
+```
+
+All these are considered to be an uncommon usage of time.Date, are reported with a 0.8 confidence.
+
+Note: even if 00, 01, 02, 03, 04, 05, 06, 07 are octal numbers, they can be considered as valid, and reported with 0.5 confidence.
+
+```go
+var _ = time.Date(2023, 01, 02, 03, 04, 00, 0, time.UTC)
+```
+
 ## time-equal
 
 _Description_: This rule warns when using `==` and `!=` for equality check `time.Time` and suggest to `time.time.Equal` method,
diff --git a/config/config.go b/config/config.go
index 34340b7..0f9ee9e 100644
--- a/config/config.go
+++ b/config/config.go
@@ -83,6 +83,7 @@ var allRules = append([]lint.Rule{
 	&rule.UselessBreak{},
 	&rule.UncheckedTypeAssertionRule{},
 	&rule.TimeEqualRule{},
+	&rule.TimeDateRule{},
 	&rule.BannedCharsRule{},
 	&rule.OptimizeOperandsOrderRule{},
 	&rule.UseAnyRule{},
diff --git a/rule/package_comments.go b/rule/package_comments.go
index 20afee8..74af626 100644
--- a/rule/package_comments.go
+++ b/rule/package_comments.go
@@ -143,7 +143,7 @@ func (l *lintPackageComments) Visit(_ ast.Node) ast.Visitor {
 		}
 	}
 
-	if l.fileAst.Doc == nil {
+	if isEmptyDoc(l.fileAst.Doc) {
 		for _, failure := range l.checkPackageComment() {
 			l.onFailure(failure)
 		}
@@ -162,3 +162,7 @@ func (l *lintPackageComments) Visit(_ ast.Node) ast.Visitor {
 	}
 	return nil
 }
+
+func isEmptyDoc(commentGroup *ast.CommentGroup) bool {
+	return commentGroup == nil || commentGroup.Text() == ""
+}
diff --git a/rule/struct_tag.go b/rule/struct_tag.go
index c886315..aa2a8ab 100644
--- a/rule/struct_tag.go
+++ b/rule/struct_tag.go
@@ -637,101 +637,127 @@ const (
 )
 
 var validateSingleOptions = map[string]struct{}{
-	"alpha":                     {},
-	"alphanum":                  {},
-	"alphanumunicode":           {},
-	"alphaunicode":              {},
-	"ascii":                     {},
-	"base32":                    {},
-	"base64":                    {},
-	"base64url":                 {},
-	"bcp47_language_tag":        {},
-	"boolean":                   {},
-	"bic":                       {},
-	"btc_addr":                  {},
-	"btc_addr_bech32":           {},
-	"cidr":                      {},
-	"cidrv4":                    {},
-	"cidrv6":                    {},
-	"country_code":              {},
-	"credit_card":               {},
-	"cron":                      {},
-	"cve":                       {},
-	"datauri":                   {},
-	"dir":                       {},
-	"dirpath":                   {},
-	"dive":                      {},
-	"dns_rfc1035_label":         {},
-	"e164":                      {},
-	"email":                     {},
-	"eth_addr":                  {},
-	"file":                      {},
-	"filepath":                  {},
-	"fqdn":                      {},
-	"hexadecimal":               {},
-	"hexcolor":                  {},
-	"hostname":                  {},
-	"hostname_port":             {},
-	"hostname_rfc1123":          {},
-	"hsl":                       {},
-	"hsla":                      {},
-	"html":                      {},
-	"html_encoded":              {},
-	"image":                     {},
-	"ip":                        {},
-	"ip4_addr":                  {},
-	"ip6_addr":                  {},
-	"ip_addr":                   {},
-	"ipv4":                      {},
-	"ipv6":                      {},
-	"isbn":                      {},
-	"isbn10":                    {},
-	"isbn13":                    {},
-	"isdefault":                 {},
-	"iso3166_1_alpha2":          {},
-	"iso3166_1_alpha3":          {},
-	"iscolor":                   {},
-	"json":                      {},
-	"jwt":                       {},
-	"latitude":                  {},
-	"longitude":                 {},
-	"lowercase":                 {},
-	"luhn_checksum":             {},
-	"mac":                       {},
-	"mongodb":                   {},
-	"mongodb_connection_string": {},
-	"multibyte":                 {},
-	"nostructlevel":             {},
-	"number":                    {},
-	"numeric":                   {},
-	"omitempty":                 {},
-	"printascii":                {},
-	"required":                  {},
-	"rgb":                       {},
-	"rgba":                      {},
-	"semver":                    {},
-	"ssn":                       {},
-	"structonly":                {},
-	"tcp_addr":                  {},
-	"tcp4_addr":                 {},
-	"tcp6_addr":                 {},
-	"timezone":                  {},
-	"udp4_addr":                 {},
-	"udp6_addr":                 {},
-	"ulid":                      {},
-	"unique":                    {},
-	"unix_addr":                 {},
-	"uppercase":                 {},
-	"uri":                       {},
-	"url":                       {},
-	"url_encoded":               {},
-	"urn_rfc2141":               {},
-	"uuid":                      {},
-	"uuid3":                     {},
-	"uuid4":                     {},
-	"uuid5":                     {},
-}
-
+	"alpha":                         {},
+	"alphanum":                      {},
+	"alphanumunicode":               {},
+	"alphaunicode":                  {},
+	"ascii":                         {},
+	"base32":                        {},
+	"base64":                        {},
+	"base64rawurl":                  {},
+	"base64url":                     {},
+	"bcp47_language_tag":            {},
+	"bic":                           {},
+	"boolean":                       {},
+	"btc_addr":                      {},
+	"btc_addr_bech32":               {},
+	"cidr":                          {},
+	"cidrv4":                        {},
+	"cidrv6":                        {},
+	"credit_card":                   {},
+	"cron":                          {},
+	"cve":                           {},
+	"datauri":                       {},
+	"dir":                           {},
+	"dirpath":                       {},
+	"dive":                          {},
+	"dns_rfc1035_label":             {},
+	"e164":                          {},
+	"ein":                           {},
+	"email":                         {},
+	"eth_addr":                      {},
+	"eth_addr_checksum":             {},
+	"file":                          {},
+	"filepath":                      {},
+	"fqdn":                          {},
+	"hexadecimal":                   {},
+	"hexcolor":                      {},
+	"hostname":                      {},
+	"hostname_port":                 {},
+	"hostname_rfc1123":              {},
+	"hsl":                           {},
+	"hsla":                          {},
+	"html":                          {},
+	"html_encoded":                  {},
+	"http_url":                      {},
+	"image":                         {},
+	"ip":                            {},
+	"ip_addr":                       {},
+	"ip4_addr":                      {},
+	"ip6_addr":                      {},
+	"ipv4":                          {},
+	"ipv6":                          {},
+	"isbn":                          {},
+	"isbn10":                        {},
+	"isbn13":                        {},
+	"isdefault":                     {},
+	"iso3166_1_alpha_numeric":       {},
+	"iso3166_1_alpha_numeric_eu":    {},
+	"iso3166_1_alpha2":              {},
+	"iso3166_1_alpha2_eu":           {},
+	"iso3166_1_alpha3":              {},
+	"iso3166_1_alpha3_eu":           {},
+	"iso3166_2":                     {},
+	"iso4217":                       {},
+	"iso4217_numeric":               {},
+	"issn":                          {},
+	"json":                          {},
+	"jwt":                           {},
+	"latitude":                      {},
+	"longitude":                     {},
+	"lowercase":                     {},
+	"luhn_checksum":                 {},
+	"mac":                           {},
+	"md4":                           {},
+	"md5":                           {},
+	"mongodb":                       {},
+	"mongodb_connection_string":     {},
+	"multibyte":                     {},
+	"number":                        {},
+	"numeric":                       {},
+	"port":                          {},
+	"postcode_iso3166_alpha2":       {},
+	"postcode_iso3166_alpha2_field": {},
+	"printascii":                    {},
+	"required":                      {},
+	"rgb":                           {},
+	"rgba":                          {},
+	"ripemd128":                     {},
+	"ripemd160":                     {},
+	"semver":                        {},
+	"sha256":                        {},
+	"sha384":                        {},
+	"sha512":                        {},
+	"ssn":                           {},
+	"tcp_addr":                      {},
+	"tcp4_addr":                     {},
+	"tcp6_addr":                     {},
+	"tiger128":                      {},
+	"tiger160":                      {},
+	"tiger192":                      {},
+	"timezone":                      {},
+	"udp_addr":                      {},
+	"udp4_addr":                     {},
+	"udp6_addr":                     {},
+	"ulid":                          {},
+	"unix_addr":                     {},
+	"uppercase":                     {},
+	"uri":                           {},
+	"url":                           {},
+	"url_encoded":                   {},
+	"urn_rfc2141":                   {},
+	"uuid":                          {},
+	"uuid_rfc4122":                  {},
+	"uuid3":                         {},
+	"uuid3_rfc4122":                 {},
+	"uuid4":                         {},
+	"uuid4_rfc4122":                 {},
+	"uuid5":                         {},
+	"uuid5_rfc4122":                 {},
+}
+
+// These are options that are used in expressions of the form
+// <option> = <RHS>
 var validateLHS = map[string]struct{}{
 	"contains":             {},
 	"containsany":          {},
@@ -741,26 +767,40 @@ var validateLHS = map[string]struct{}{
 	"endsnotwith":          {},
 	"endswith":             {},
 	"eq":                   {},
-	"eqfield":              {},
+	"eq_ignore_case":       {},
 	"eqcsfield":            {},
+	"eqfield":              {},
 	"excluded_if":          {},
 	"excluded_unless":      {},
+	"excluded_with":        {},
+	"excluded_with_all":    {},
+	"excluded_without":     {},
+	"excluded_without_all": {},
 	"excludes":             {},
 	"excludesall":          {},
 	"excludesfield":        {},
 	"excludesrune":         {},
+	"fieldcontains":        {},
+	"fieldexcludes":        {},
 	"gt":                   {},
 	"gtcsfield":            {},
+	"gte":                  {},
 	"gtecsfield":           {},
+	"gtefield":             {},
+	"gtfield":              {},
 	"len":                  {},
 	"lt":                   {},
-	"lte":                  {},
 	"ltcsfield":            {},
+	"lte":                  {},
 	"ltecsfield":           {},
+	"ltefield":             {},
+	"ltfield":              {},
 	"max":                  {},
 	"min":                  {},
 	"ne":                   {},
+	"ne_ignore_case":       {},
 	"necsfield":            {},
+	"nefield":              {},
 	"oneof":                {},
 	"oneofci":              {},
 	"required_if":          {},
@@ -769,8 +809,10 @@ var validateLHS = map[string]struct{}{
 	"required_with_all":    {},
 	"required_without":     {},
 	"required_without_all": {},
+	"skip_unless":          {},
 	"spicedb":              {},
 	"startsnotwith":        {},
 	"startswith":           {},
 	"unique":               {},
+	"validateFn":           {},
 }
diff --git a/rule/time_date.go b/rule/time_date.go
new file mode 100644
index 0000000..c8ba841
--- /dev/null
+++ b/rule/time_date.go
@@ -0,0 +1,237 @@
+package rule
+
+import (
+	"errors"
+	"fmt"
+	"go/ast"
+	"go/token"
+	"strconv"
+	"strings"
+
+	"github.com/mgechev/revive/lint"
+	"github.com/mgechev/revive/logging"
+)
+
+// TimeDateRule lints the way time.Date is used.
+type TimeDateRule struct{}
+
+// Apply applies the rule to given file.
+func (*TimeDateRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
+	var failures []lint.Failure
+
+	onFailure := func(failure lint.Failure) {
+		failures = append(failures, failure)
+	}
+
+	w := &lintTimeDate{file, onFailure}
+
+	ast.Walk(w, file.AST)
+	return failures
+}
+
+// Name returns the rule name.
+func (*TimeDateRule) Name() string {
+	return "time-date"
+}
+
+type lintTimeDate struct {
+	file      *lint.File
+	onFailure func(lint.Failure)
+}
+
+var (
+	// timeDateArgumentNames are the names of the arguments of time.Date
+	timeDateArgumentNames = []string{
+		"year",
+		"month",
+		"day",
+		"hour",
+		"minute",
+		"second",
+		"nanosecond",
+		"timezone",
+	}
+
+	// timeDateArity is the number of arguments of time.Date
+	timeDateArity = len(timeDateArgumentNames)
+)
+
+func (w lintTimeDate) Visit(n ast.Node) ast.Visitor {
+	ce, ok := n.(*ast.CallExpr)
+	if !ok || len(ce.Args) != timeDateArity {
+		return w
+	}
+	if !isPkgDot(ce.Fun, "time", "Date") {
+		return w
+	}
+
+	// The last argument is a timezone, there is no need to check it, also it has a different type
+	for pos, arg := range ce.Args[:timeDateArity-1] {
+		bl, ok := arg.(*ast.BasicLit)
+		if !ok {
+			continue
+		}
+
+		replacedValue, err := parseDecimalInteger(bl)
+		if err == nil {
+			continue
+		}
+
+		if errors.Is(err, errParsedInvalid) {
+			// This is not supposed to happen, let's be defensive
+			// log the error, but continue
+
+			logger, errLogger := logging.GetLogger()
+			if errLogger != nil {
+				// This is not supposed to happen, discard both errors
+				continue
+			}
+			logger.With(
+				"value", bl.Value,
+				"kind", bl.Kind,
+				"error", err.Error(),
+			).Error("failed to parse time.Date argument")
+
+			continue
+		}
+
+		confidence := 0.8 // default confidence
+		errMessage := err.Error()
+		instructions := fmt.Sprintf("use %s instead of %s", replacedValue, bl.Value)
+		switch {
+		case errors.Is(err, errParsedOctalWithZero):
+			// people can use 00, 01, 02, 03, 04, 05, 06, and 07 if they want.
+			confidence = 0.5
+
+		case errors.Is(err, errParsedOctalWithPaddingZeroes):
+			// This is a clear mistake.
+			// example with 000123456 (octal) is about 123456 or 42798 ?
+			confidence = 1
+
+			strippedValue := strings.TrimLeft(bl.Value, "0")
+			if strippedValue == "" {
+				// avoid issue with 00000000
+				strippedValue = "0"
+			}
+
+			if strippedValue != replacedValue {
+				instructions = fmt.Sprintf(
+					"choose between %s and %s (decimal value of %s octal value)",
+					strippedValue, replacedValue, strippedValue,
+				)
+			}
+		}
+
+		w.onFailure(lint.Failure{
+			Category:   "time",
+			Node:       bl,
+			Confidence: confidence,
+			Failure: fmt.Sprintf(
+				"use decimal digits for time.Date %s argument: %s found: %s",
+				timeDateArgumentNames[pos], errMessage, instructions),
+		})
+	}
+
+	return w
+}
+
+var (
+	errParsedOctal                  = errors.New("octal notation")
+	errParsedOctalWithZero          = errors.New("octal notation with leading zero")
+	errParsedOctalWithPaddingZeroes = errors.New("octal notation with padding zeroes")
+	errParsedHexadecimal            = errors.New("hexadecimal notation")
+	errParseBinary                  = errors.New("binary notation")
+	errParsedFloat                  = errors.New("float literal")
+	errParsedExponential            = errors.New("exponential notation")
+	errParsedAlternative            = errors.New("alternative notation")
+	errParsedInvalid                = errors.New("invalid notation")
+)
+
+func parseDecimalInteger(bl *ast.BasicLit) (string, error) {
+	currentValue := strings.ToLower(bl.Value)
+
+	switch currentValue {
+	case "0":
+		// skip 0 as it is a valid value for all the arguments
+		return bl.Value, nil
+	case "00", "01", "02", "03", "04", "05", "06", "07":
+		// people can use 00, 01, 02, 03, 04, 05, 06, 07, if they want
+		return bl.Value[1:], errParsedOctalWithZero
+	}
+
+	switch bl.Kind {
+	case token.FLOAT:
+		// someone used a float literal, while they should have used an integer literal.
+		parsedValue, err := strconv.ParseFloat(currentValue, 64)
+		if err != nil {
+			// This is not supposed to happen
+			return bl.Value, fmt.Errorf(
+				"%w: %s: %w",
+				errParsedInvalid,
+				"failed to parse number as float",
+				err,
+			)
+		}
+
+		// this will convert back the number to a string
+		cleanedValue := strconv.FormatFloat(parsedValue, 'f', -1, 64)
+		if strings.Contains(currentValue, "e") {
+			return cleanedValue, errParsedExponential
+		}
+
+		return cleanedValue, errParsedFloat
+
+	case token.INT:
+		// we expect this format
+
+	default:
+		// This is not supposed to happen
+		return bl.Value, fmt.Errorf(
+			"%w: %s",
+			errParsedInvalid,
+			"unexpected kind of literal",
+		)
+	}
+
+	// Parse the number with base=0 that allows to accept all number formats and base
+	parsedValue, err := strconv.ParseInt(currentValue, 0, 64)
+	if err != nil {
+		// This is not supposed to happen
+		return bl.Value, fmt.Errorf(
+			"%w: %s: %w",
+			errParsedInvalid,
+			"failed to parse number as integer",
+			err,
+		)
+	}
+
+	cleanedValue := strconv.FormatInt(parsedValue, 10)
+
+	// Let's figure out the notation to return an error
+	switch {
+	case strings.HasPrefix(currentValue, "0b"):
+		return cleanedValue, errParseBinary
+	case strings.HasPrefix(currentValue, "0x"):
+		return cleanedValue, errParsedHexadecimal
+	case strings.HasPrefix(currentValue, "0"):
+		// this matches both "0" and "0o" octal notation.
+
+		if strings.HasPrefix(currentValue, "00") {
+			// 00123456 (octal) is about 123456 or 42798 ?
+			return cleanedValue, errParsedOctalWithPaddingZeroes
+		}
+
+		// 0006 is a valid octal notation, but we can use 6 instead.
+		return cleanedValue, errParsedOctal
+	}
+
+	// Convert back the number to a string, and compare it with the original one
+	formattedValue := strconv.FormatInt(parsedValue, 10)
+	if formattedValue != currentValue {
+		// This can catch some edge cases like: 1_0 ...
+		return cleanedValue, errParsedAlternative
+	}
+
+	// The number is a decimal integer, we can use it as is.
+	return bl.Value, nil
+}


Test patch:
diff --git a/test/time_date_test.go b/test/time_date_test.go
new file mode 100644
index 0000000..6cb98db
--- /dev/null
+++ b/test/time_date_test.go
@@ -0,0 +1,11 @@
+package test
+
+import (
+	"testing"
+
+	"github.com/mgechev/revive/rule"
+)
+
+func TestTimeDate(t *testing.T) {
+	testRule(t, "time_date_decimal_literal", &rule.TimeDateRule{})
+}
diff --git a/testdata/golint/package_doc7.go b/testdata/golint/package_doc7.go
new file mode 100644
index 0000000..db191ee
--- /dev/null
+++ b/testdata/golint/package_doc7.go
@@ -0,0 +1,2 @@
+//nolint:dupl
+package foo
diff --git a/testdata/struct_tag.go b/testdata/struct_tag.go
index c8174db..e9d9933 100644
--- a/testdata/struct_tag.go
+++ b/testdata/struct_tag.go
@@ -159,6 +159,7 @@ type ValidateUser struct {
 	BadComplex  string `validate:"gt=0,keys,eq=1|eq=2,endkeys,required"`              // MATCH /option "keys" must follow a "dive" option in validate tag/
 	BadComplex2 string `validate:"gt=0,dive,eq=1|eq=2,endkeys,required"`              // MATCH /option "endkeys" without a previous "keys" option in validate tag/
 	BadComplex3 string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,endkeys,required"` // MATCH /option "endkeys" without a previous "keys" option in validate tag/
+	Issue1367   string `validate:"required_without=ExternalValue,excluded_with=ExternalValue"`
 }
 
 type TomlUser struct {
diff --git a/testdata/time_date_decimal_literal.go b/testdata/time_date_decimal_literal.go
new file mode 100644
index 0000000..d2b9526
--- /dev/null
+++ b/testdata/time_date_decimal_literal.go
@@ -0,0 +1,108 @@
+package pkg
+
+import "time"
+
+// these should match and be reported as invalid usage of time.Date
+// the rule should suggest to use a decimal number
+var (
+	// All these examples refers to the same date
+	// 2023-01-02 03:04:05.000000006 +0000 UTC
+
+	// this is an invalid usage
+	// people are using this format because a date is easier to read
+	// but here it leads to use octal numbers0
+	_ = time.Date(
+		2023,
+		01,        // MATCH /use decimal digits for time.Date month argument: octal notation with leading zero found: use 1 instead of 01/
+		02,        // MATCH /use decimal digits for time.Date day argument: octal notation with leading zero found: use 2 instead of 02/
+		03,        // MATCH /use decimal digits for time.Date hour argument: octal notation with leading zero found: use 3 instead of 03/
+		04,        // MATCH /use decimal digits for time.Date minute argument: octal notation with leading zero found: use 4 instead of 04/
+		05,        // MATCH /use decimal digits for time.Date second argument: octal notation with leading zero found: use 5 instead of 05/
+		000000006, // MATCH /use decimal digits for time.Date nanosecond argument: octal notation with padding zeroes found: use 6 instead of 000000006/
+		time.UTC)
+
+	// the following one could have been written by someone who is not aware of the issue
+	// Please note, there are multiple issues on the same line
+	//
+	// use special syntax to match multiple issues being reported on the same line
+	// MATCH:34 /use decimal digits for time.Date month argument: octal notation with leading zero found: use 1 instead of 01/
+	// MATCH:34 /use decimal digits for time.Date day argument: octal notation with leading zero found: use 2 instead of 02/
+	// MATCH:34 /use decimal digits for time.Date hour argument: octal notation with leading zero found: use 3 instead of 03/
+	// MATCH:34 /use decimal digits for time.Date minute argument: octal notation with leading zero found: use 4 instead of 04/
+	// MATCH:34 /use decimal digits for time.Date second argument: octal notation with leading zero found: use 5 instead of 05/
+	// MATCH:34 /use decimal digits for time.Date nanosecond argument: octal notation with padding zeroes found: use 6 instead of 000000006/
+	_ = time.Date(2023, 01, 02, 03, 04, 05, 000000006, time.UTC)
+)
+
+// gofumpt formats legacy non-decimal notation to new one non-decimal notation
+// it transforms 01, 02, 03, 04, 05, 06, and 07 to 0o1, 0o2, 0o3, 0o4, 0o5, 0o6, and 0o7
+// but here with time.Date it doesn't make sense. This is the main reason why the rule was created.
+var (
+	_ = time.Date(2023, 0o1, 2, 3, 4, 5, 6, time.UTC) // MATCH /use decimal digits for time.Date month argument: octal notation found: use 1 instead of 0o1/
+	_ = time.Date(2023, 1, 0o2, 3, 4, 5, 6, time.UTC) // MATCH /use decimal digits for time.Date day argument: octal notation found: use 2 instead of 0o2/
+	_ = time.Date(2023, 1, 2, 0o3, 4, 5, 6, time.UTC) // MATCH /use decimal digits for time.Date hour argument: octal notation found: use 3 instead of 0o3/
+	_ = time.Date(2023, 1, 2, 3, 0o4, 5, 6, time.UTC) // MATCH /use decimal digits for time.Date minute argument: octal notation found: use 4 instead of 0o4/
+	_ = time.Date(2023, 1, 2, 3, 4, 0o5, 6, time.UTC) // MATCH /use decimal digits for time.Date second argument: octal notation found: use 5 instead of 0o5/
+	_ = time.Date(2023, 1, 2, 3, 4, 5, 0o6, time.UTC) // MATCH /use decimal digits for time.Date nanosecond argument: octal notation found: use 6 instead of 0o6/
+)
+
+// padding with zeroes can lead to errors
+var (
+	_ = time.Date(2023, 1, 2, 3, 4, 5, 00, time.UTC) // MATCH /use decimal digits for time.Date nanosecond argument: octal notation with leading zero found: use 0 instead of 00/
+	_ = time.Date(2023, 1, 2, 3, 4, 5, 01, time.UTC) // MATCH /use decimal digits for time.Date nanosecond argument: octal notation with leading zero found: use 1 instead of 01/
+
+	_ = time.Date(2023, 1, 2, 3, 4, 5, 00000000, time.UTC) // MATCH /use decimal digits for time.Date nanosecond argument: octal notation with padding zeroes found: use 0 instead of 00000000/
+	_ = time.Date(2023, 1, 2, 3, 4, 5, 00000006, time.UTC) // MATCH /use decimal digits for time.Date nanosecond argument: octal notation with padding zeroes found: use 6 instead of 00000006/
+	_ = time.Date(2023, 1, 2, 3, 4, 5, 00123456, time.UTC) // MATCH /use decimal digits for time.Date nanosecond argument: octal notation with padding zeroes found: choose between 123456 and 42798 (decimal value of 123456 octal value)/
+)
+
+// hypothetical examples based on other number notations
+// https://go.dev/ref/spec#Integer_literals
+// these should match and be reported as invalid usage of time.Date
+var (
+	_ = time.Date(
+		0x7e7,    // MATCH /use decimal digits for time.Date year argument: hexadecimal notation found: use 2023 instead of 0x7e7/
+		0b1,      // MATCH /use decimal digits for time.Date month argument: binary notation found: use 1 instead of 0b1/
+		0x_2,     // MATCH /use decimal digits for time.Date day argument: hexadecimal notation found: use 2 instead of 0x_2/
+		1_3,      // MATCH /use decimal digits for time.Date hour argument: alternative notation found: use 13 instead of 1_3/
+		1e1,      // MATCH /use decimal digits for time.Date minute argument: exponential notation found: use 10 instead of 1e1/
+		0.,       // MATCH /use decimal digits for time.Date second argument: float literal found: use 0 instead of 0./
+		0x1.Fp+6, // MATCH /use decimal digits for time.Date nanosecond argument: float literal found: use 124 instead of 0x1.Fp+6/
+		time.UTC)
+)
+
+// these should never match
+var (
+	_ = time.Date(2023, 1, 2, 3, 4, 5, 1234567, time.UTC)
+	_ = time.Date(2023, time.January, 2, 3, 4, 5, 1234567, time.UTC)
+	_ = time.Date(2023, 10, 10, 10, 10, 10, 100, time.UTC)
+	_ = time.Date(2023, 1, 2, 3, 4, 5, 0, time.UTC)
+	_ = time.Date(2023, 1, 2, 3, 4, 5, 6, time.UTC)
+)
+
+// uncommon notations
+// these notations are supported by Go, the rule should not report it
+// it's out of the scope of the rule
+var (
+	// negative values can be used
+	// Go will compute 2022-10-28 20:55:54.999999994 +0000 UTC
+	_ = time.Date(2023, -1, -2, -3, -4, -5, -6, time.UTC)
+
+	// value out of the expected scale for a field, are OK.
+	// Go will compute 2024-02-02 04:11:10 +0000 UTC
+	_ = time.Date(2023, 13, 32, 27, 70, 70, 0, time.UTC)
+
+	// using gigantic nanoseconds to move into the future
+	// Go will compute 2054-09-10 04:50:45 +0000 UTC
+	_ = time.Date(2023, 1, 2, 3, 4, 5, 1000000000000000000, time.UTC)
+)
+
+// Common user errors with dates
+// it's out of the scope of the rule
+var (
+	// there is no February 29th in 2023, Go will compute 2023-03-01 03:04:05 +0000 UTC
+	_ = time.Date(2023, 2, 29, 3, 4, 5, 0, time.UTC)
+
+	// June has only 30 days, Go will compute 2023-07-01 03:04:05 +0000 UTC
+	_ = time.Date(2023, 6, 31, 3, 4, 5, 0, time.UTC)
+)