Repository: mgechev/revive (go)

Issue:
time-date: add out of bound detection
**Is your feature request related to a problem? Please describe.**

Right now, some people are inverting month and day in the time.Date argument.

It's because the ISO 8601 format (YYYY-MM-DD) is not obvious for people using MM/DD/YYYY format

Here are some examples

https://github.com/search?q=language%3Ago+%2Ftime%5C.Date%5C%282%5Cd%5Cd%5Cd%2C+%281%5B3-9%5D%7C2%5Cd%7C3%5Cd%29%2F&type=code

**Describe the solution you'd like**

Report when arguments are bigger than the expected range

months: 1-12 [examples](https://github.com/search?q=language%3Ago+%2Ftime%5C.Date%5C%282%5Cd%5Cd%5Cd%2C+%281%5B3-9%5D%7C2%5Cd%7C3%5Cd%29%2F&type=code)
day: 1-31 (depending on the months and leap years)  [examples](https://github.com/search?q=language%3Ago+%2Ftime%5C.Date%5C%282%5Cd%5Cd%5Cd%2C+%5Cd%5Cd%3F%2C+%283%5B2-9%5D%7C%5B4-5%5D%5Cd%29%2F&type=code)
Hour: 0-23 [examples](https://github.com/search?q=language%3Ago+%2Ftime%5C.Date%5C%282%5Cd%5Cd%5Cd%2C+%5Cd%5Cd%3F%2C+%5Cd%5Cd%3F%2C+%282%5B4-9%5D%7C%5B3-9%5D%5Cd%29%2F&type=code)
Minutes: 0-59 [examples](https://github.com/search?q=language%3Ago+%2Ftime%5C.Date%5C%282%5Cd%5Cd%5Cd%2C+%5Cd%5Cd%3F%2C+%5Cd%5Cd%3F%2C+%5Cd%5Cd%3F%2C+%28%5B6-9%5D%5Cd%29%2F&type=code)
Second: 0-59 (plus the leap second when it exists, for 23:59:60) [examples](https://github.com/search?q=language%3Ago+%2Ftime%5C.Date%5C%282%5Cd%5Cd%5Cd%2C+%5Cd%5Cd%3F%2C+%5Cd%5Cd%3F%2C+%5Cd%5Cd%3F%2C+%5Cd%5Cd%3F%2C+%286%5B2-9%5D%7C%5B7-9%5D%5Cd%29%2F&type=code) 

**Describe alternatives you've considered**

**Additional context**

I don't think there is a need to detect leap seconds and leap year for now, we can limit ourself to month having 31 days.

Or we can simply detect when months is higher than 12.


Pull request:
feature: detect time/date out of bounds in time-date rule
<!-- ### IMPORTANT ### -->
<!-- Please do not create a Pull Request without creating an issue first.** -->
<!-- If you're fixing a typo or improving the documentation, you may not have to open an issue. -->

<!-- ### CHECKLIST ### -->
<!-- Please, describe in details what's your motivation for this PR -->
<!-- Did you add tests? -->
<!-- Does your code follow the coding style of the rest of the repository? -->
<!-- Does the GitHub Action build passes? -->

<!-- ### FOOTER (OPTIONAL) ### -->
<!-- If you're closing an issue, add "Closes #XXXX" in your comment. This way, the PR will be linked to the issue automatically. -->

dates are checked with the following rules:
- no validation for year
- month must be between 1 and 12
- day must be between 1 and 31 for January, March, May, July, August, October, December
- day must be between 1 and 30 for April, June, September, November
- day must be between 1 and 28 for February, or 29 for leap years
- hour must be between 0 and 23
- minute must be between 0 and 59
- second must be between 0 and 59 (and 60 for leap seconds)
- nanosecond must be between 0 and 999999999, because otherwise you can use a second

Fixes #1369 

- #1369

Patch:
diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md
index b26d94f..943dc68 100644
--- a/RULES_DESCRIPTIONS.md
+++ b/RULES_DESCRIPTIONS.md
@@ -1065,9 +1065,25 @@ _Description_: Reports bad usage of `time.Date`.
 
 _Configuration_: N/A
 
-_Example_:
+_Examples_:
 
-Here the leading zeros are defining integers with octal notation
+- Invalid dates reporting:
+
+  - 0 for the month or day argument
+  - out of bounds argument for the month (12), day (31), hour (23), minute (59), or seconds (59)
+  - an invalid date: 31st of June, 29th of February in 2023, ...
+
+- Non-decimal integers are used as arguments
+
+  This includes:
+
+  - leading zero notation like using 00 for hours, minutes, and seconds.
+  - octal notation 0o1, 0o0 that are often caused by using gofumpt on leading zero notation.
+  - padding zeros such as 00123456 that are source of bugs.
+  - ... and some other use cases.
+
+<details>
+<summary>More information about what is detected and reported</summary>
 
 ```go
 import "time"
@@ -1114,6 +1130,8 @@ import "time"
 var _ = time.Date(2023, 01, 02, 03, 04, 00, 0, time.UTC)
 ```
 
+</details>
+
 ## time-equal
 
 _Description_: This rule warns when using `==` and `!=` for equality check `time.Time` and suggest to `time.time.Equal` method,
diff --git a/rule/time_date.go b/rule/time_date.go
index ebcb083..939ea82 100644
--- a/rule/time_date.go
+++ b/rule/time_date.go
@@ -7,6 +7,7 @@ import (
 	"go/token"
 	"strconv"
 	"strings"
+	"time"
 
 	"github.com/mgechev/revive/internal/astutils"
 	"github.com/mgechev/revive/lint"
@@ -40,23 +41,51 @@ type lintTimeDate struct {
 	onFailure func(lint.Failure)
 }
 
+// timeDateArgument is a type for the arguments of time.Date function.
+type timeDateArgument string
+
+const (
+	timeDateArgYear       timeDateArgument = "year"
+	timeDateArgMonth      timeDateArgument = "month"
+	timeDateArgDay        timeDateArgument = "day"
+	timeDateArgHour       timeDateArgument = "hour"
+	timeDateArgMinute     timeDateArgument = "minute"
+	timeDateArgSecond     timeDateArgument = "second"
+	timeDateArgNanosecond timeDateArgument = "nanosecond"
+	timeDateArgTimezone   timeDateArgument = "timezone"
+)
+
 var (
 	// timeDateArgumentNames are the names of the arguments of time.Date.
-	timeDateArgumentNames = []string{
-		"year",
-		"month",
-		"day",
-		"hour",
-		"minute",
-		"second",
-		"nanosecond",
-		"timezone",
+	timeDateArgumentNames = []timeDateArgument{
+		timeDateArgYear,
+		timeDateArgMonth,
+		timeDateArgDay,
+		timeDateArgHour,
+		timeDateArgMinute,
+		timeDateArgSecond,
+		timeDateArgNanosecond,
+		timeDateArgTimezone,
 	}
 
 	// timeDateArity is the number of arguments of time.Date.
 	timeDateArity = len(timeDateArgumentNames)
 )
 
+var timeDateArgumentBoundaries = map[timeDateArgument][2]int64{
+	// year is not validated
+	timeDateArgMonth:      {1, 12},
+	timeDateArgDay:        {1, 31}, // there is a special check for this field, this is just a fallback
+	timeDateArgHour:       {0, 23},
+	timeDateArgMinute:     {0, 59},
+	timeDateArgSecond:     {0, 60},      // 60 is for leap second
+	timeDateArgNanosecond: {0, 1e9 - 1}, // 1e9 is not allowed, as it means 1 second
+}
+
+type timeDateMonthYear struct {
+	year, month int64
+}
+
 func (w lintTimeDate) Visit(n ast.Node) ast.Visitor {
 	ce, ok := n.(*ast.CallExpr)
 	if !ok || len(ce.Args) != timeDateArity {
@@ -77,15 +106,127 @@ func (w lintTimeDate) Visit(n ast.Node) ast.Visitor {
 		})
 	}
 
+	var parsedDate timeDateMonthYear
 	// All the other arguments should be decimal integers.
 	for pos, arg := range ce.Args[:timeDateArity-1] {
-		bl, ok := arg.(*ast.BasicLit)
+		fieldName := timeDateArgumentNames[pos]
+
+		bl, ok := w.checkArgSign(arg, fieldName)
 		if !ok {
+			// either it is not a basic literal
+			// or it is a unary expression with a sign that was reported as a failure
 			continue
 		}
 
-		replacedValue, err := parseDecimalInteger(bl)
+		parsedValue, err := parseDecimalInteger(bl)
 		if err == nil {
+			if fieldName == timeDateArgYear {
+				// store the year value for further checks with day
+				parsedDate.year = parsedValue
+
+				// no checks for year, as it can be any value
+				// a year can be negative, zero, or positive
+				continue
+			}
+
+			boundaries, ok := timeDateArgumentBoundaries[fieldName]
+			if !ok {
+				// no boundaries for this field, skip it
+				continue
+			}
+			minValue, maxValue := boundaries[0], boundaries[1]
+
+			switch fieldName {
+			case timeDateArgMonth:
+				parsedDate.month = parsedValue
+
+				if parsedValue == 0 {
+					// Special case: month is 0.
+					// Go treats it as January, but we still report it as a failure.
+					w.onFailure(lint.Failure{
+						Category:   "time",
+						Node:       arg,
+						Confidence: 1,
+						Failure:    "time.Date month argument should not be zero",
+					})
+					continue
+				}
+
+			case timeDateArgDay:
+
+				switch {
+				case parsedValue == 0:
+					// Special case: day is 0.
+					// Go treats it as the first day of the month, but we still report it as a failure.
+					w.onFailure(lint.Failure{
+						Category:   "time",
+						Node:       arg,
+						Confidence: 1,
+						Failure:    "time.Date day argument should not be zero",
+					})
+					continue
+
+				// the month is valid, check the day
+				case parsedDate.month >= 1 && parsedDate.month <= 12:
+					month := time.Month(parsedDate.month)
+
+					maxValue = w.daysInMonth(parsedDate.year, month)
+
+					monthName := month.String()
+					if month == time.February {
+						// because of leap years, we need to provide the year in the error message
+						monthName += " " + strconv.FormatInt(parsedDate.year, 10)
+					}
+
+					if parsedValue > maxValue {
+						// we can provide a more detailed error message
+						w.onFailure(lint.Failure{
+							Category:   "time",
+							Node:       arg,
+							Confidence: 0.8,
+							Failure: fmt.Sprintf(
+								"time.Date day argument is %d, but %s has only %d days",
+								parsedValue, monthName, maxValue,
+							),
+						})
+						continue
+					}
+
+				// We know, the month is >12, let's try to detect possible day and month swap in arguments.
+				// for example: time.Date(2023, 31, 6, 0, 0, 0, 0, time.UTC)
+				case parsedDate.month > 12 && parsedDate.month <= 31 && parsedValue <= 12:
+
+					// Invert the month and day values
+					realMonth, realDay := parsedValue, parsedDate.month
+
+					// Check if the real month is valid.
+					if realDay <= w.daysInMonth(parsedDate.year, time.Month(realMonth)) {
+						w.onFailure(lint.Failure{
+							Category:   "time",
+							Node:       arg,
+							Confidence: 0.5,
+							Failure: fmt.Sprintf(
+								"time.Date month and day arguments appear to be swapped: %d-%02d-%02d vs %d-%02d-%02d",
+								parsedDate.year, realMonth, realDay,
+								parsedDate.year, parsedDate.month, parsedValue,
+							),
+						})
+					}
+				}
+			}
+
+			if parsedValue < minValue || parsedValue > maxValue {
+				w.onFailure(lint.Failure{
+					Category:   "time",
+					Node:       arg,
+					Confidence: 0.8,
+					Failure: fmt.Sprintf(
+						"time.Date %s argument should be between %d and %d: %s",
+						fieldName, minValue, maxValue, astutils.GoFmt(arg),
+					),
+				})
+			}
+
 			continue
 		}
 
@@ -109,7 +250,8 @@ func (w lintTimeDate) Visit(n ast.Node) ast.Visitor {
 
 		confidence := 0.8 // default confidence
 		errMessage := err.Error()
-		instructions := fmt.Sprintf("use %s instead of %s", replacedValue, bl.Value)
+		replacedValue := strconv.FormatInt(parsedValue, 10)
+		instructions := fmt.Sprintf("use %s instead of %s", replacedValue, astutils.GoFmt(arg))
 		switch {
 		case errors.Is(err, errParsedOctalWithZero):
 			// people can use 00, 01, 02, 03, 04, 05, 06, and 07 if they want.
@@ -140,13 +282,107 @@ func (w lintTimeDate) Visit(n ast.Node) ast.Visitor {
 			Confidence: confidence,
 			Failure: fmt.Sprintf(
 				"use decimal digits for time.Date %s argument: %s found: %s",
-				timeDateArgumentNames[pos], errMessage, instructions),
+				fieldName, errMessage, instructions),
 		})
 	}
 
 	return w
 }
 
+func (w *lintTimeDate) checkArgSign(arg ast.Node, fieldName timeDateArgument) (*ast.BasicLit, bool) {
+	if bl, ok := arg.(*ast.BasicLit); ok {
+		// it is an unsigned basic literal
+		// we can use it as is
+		return bl, true
+	}
+
+	// We can have an unary expression like -1, -a, +a, +1...
+	node, ok := arg.(*ast.UnaryExpr)
+	if !ok {
+		// Any other expression is not supported.
+		// It could be something like this:
+		// time.Date(2023, 2 * a, 3 + b, 4, 5, 6, 7, time.UTC)
+		return nil, false
+	}
+
+	// But we expect the unary expression to be followed by a basic literal
+	bl, ok := node.X.(*ast.BasicLit)
+	if !ok {
+		// This is not a basic literal, it could be an identifier, a function call, etc.
+		// -a
+		// ^b
+		// -foo()
+		//
+		// It's out of scope of this rule.
+		return nil, false
+	}
+	// So now, we have an unary expression like -500, +2023, -0x1234 ...
+
+	if fieldName == timeDateArgYear && node.Op == token.SUB {
+		// The year can be negative, like referring to BC years.
+		// We can return it as is, without reporting a failure
+		return bl, true
+	}
+
+	switch node.Op {
+	case token.SUB:
+		// This is a negative number, it is supported, but it's uncommon.
+		w.onFailure(lint.Failure{
+			Category:   "time",
+			Node:       arg,
+			Confidence: 0.5,
+			Failure: fmt.Sprintf(
+				"time.Date %s argument is negative: %s",
+				fieldName, astutils.GoFmt(arg),
+			),
+		})
+	case token.ADD:
+		// There is a positive sign, but it is not necessary to have a positive sign
+		w.onFailure(lint.Failure{
+			Category:   "time",
+			Node:       arg,
+			Confidence: 0.8,
+			Failure: fmt.Sprintf(
+				"time.Date %s argument contains a useless plus sign: %s",
+				fieldName, astutils.GoFmt(arg),
+			),
+		})
+	default:
+		// Other unary expressions are not supported.
+		//
+		// It could be something like this:
+		// ^1, ^0x1234
+		// but these are unlikely to be used with time.Date
+		// We ignore them, to avoid false positives.
+	}
+
+	return nil, false
+}
+
+// isLeapYear checks if the year is a leap year.
+// This is used to check if the date is valid according to Go implementation.
+func (lintTimeDate) isLeapYear(year int64) bool {
+	// We cannot use the classic formula of
+	// year%4 == 0 && (year%100 != 0 || year%400 == 0)
+	// because we want to ensure what time.Date will compute
+
+	return time.Date(int(year), 2, 29, 0, 0, 0, 0, time.UTC).Format("01-02") == "02-29"
+}
+
+func (w lintTimeDate) daysInMonth(year int64, month time.Month) int64 {
+	switch month {
+	case time.April, time.June, time.September, time.November:
+		return 30
+	case time.February:
+		if w.isLeapYear(year) {
+			return 29
+		}
+		return 28
+	}
+
+	return 31
+}
+
 var (
 	errParsedOctal                  = errors.New("octal notation")
 	errParsedOctalWithZero          = errors.New("octal notation with leading zero")
@@ -159,16 +395,12 @@ var (
 	errParsedInvalid                = errors.New("invalid notation")
 )
 
-func parseDecimalInteger(bl *ast.BasicLit) (string, error) {
+func parseDecimalInteger(bl *ast.BasicLit) (int64, error) {
 	currentValue := strings.ToLower(bl.Value)
 
-	switch currentValue {
-	case "0":
+	if currentValue == "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
+		return 0, nil
 	}
 
 	switch bl.Kind {
@@ -177,7 +409,7 @@ func parseDecimalInteger(bl *ast.BasicLit) (string, error) {
 		parsedValue, err := strconv.ParseFloat(currentValue, 64)
 		if err != nil {
 			// This is not supposed to happen
-			return bl.Value, fmt.Errorf(
+			return 0, fmt.Errorf(
 				"%w: %s: %w",
 				errParsedInvalid,
 				"failed to parse number as float",
@@ -186,19 +418,18 @@ func parseDecimalInteger(bl *ast.BasicLit) (string, error) {
 		}
 
 		// 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 int64(parsedValue), errParsedExponential
 		}
 
-		return cleanedValue, errParsedFloat
+		return int64(parsedValue), errParsedFloat
 
 	case token.INT:
 		// we expect this format
 
 	default:
 		// This is not supposed to happen
-		return bl.Value, fmt.Errorf(
+		return 0, fmt.Errorf(
 			"%w: %s",
 			errParsedInvalid,
 			"unexpected kind of literal",
@@ -209,7 +440,7 @@ func parseDecimalInteger(bl *ast.BasicLit) (string, error) {
 	parsedValue, err := strconv.ParseInt(currentValue, 0, 64)
 	if err != nil {
 		// This is not supposed to happen
-		return bl.Value, fmt.Errorf(
+		return 0, fmt.Errorf(
 			"%w: %s: %w",
 			errParsedInvalid,
 			"failed to parse number as integer",
@@ -217,33 +448,35 @@ func parseDecimalInteger(bl *ast.BasicLit) (string, error) {
 		)
 	}
 
-	cleanedValue := strconv.FormatInt(parsedValue, 10)
-
 	// Let's figure out the notation to return an error
 	switch {
 	case strings.HasPrefix(currentValue, "0b"):
-		return cleanedValue, errParseBinary
+		return parsedValue, errParseBinary
 	case strings.HasPrefix(currentValue, "0x"):
-		return cleanedValue, errParsedHexadecimal
+		return parsedValue, errParsedHexadecimal
 	case strings.HasPrefix(currentValue, "0"):
 		// this matches both "0" and "0o" octal notation.
 
+		switch currentValue {
+		// people can use 00, 01, 02, 03, 04, 05, 06, 07, if they want
+		case "00", "01", "02", "03", "04", "05", "06", "07":
+			return parsedValue, errParsedOctalWithZero
+		}
+
 		if strings.HasPrefix(currentValue, "00") {
 			// 00123456 (octal) is about 123456 or 42798 ?
-			return cleanedValue, errParsedOctalWithPaddingZeroes
+			return parsedValue, errParsedOctalWithPaddingZeroes
 		}
 
-		// 0006 is a valid octal notation, but we can use 6 instead.
-		return cleanedValue, errParsedOctal
+		return parsedValue, 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
+		return parsedValue, errParsedAlternative
 	}
 
-	// The number is a decimal integer, we can use it as is.
-	return bl.Value, nil
+	return parsedValue, nil
 }


Test patch:
diff --git a/test/time_date_test.go b/test/time_date_test.go
index c379a9d..eb4ba6d 100644
--- a/test/time_date_test.go
+++ b/test/time_date_test.go
@@ -9,4 +9,5 @@ import (
 func TestTimeDate(t *testing.T) {
 	testRule(t, "time_date_decimal_literal", &rule.TimeDateRule{})
 	testRule(t, "time_date_nil_timezone", &rule.TimeDateRule{})
+	testRule(t, "time_date_out_of_bounds", &rule.TimeDateRule{})
 }
diff --git a/testdata/time_date_decimal_literal.go b/testdata/time_date_decimal_literal.go
index f63ad39..8b82fbe 100644
--- a/testdata/time_date_decimal_literal.go
+++ b/testdata/time_date_decimal_literal.go
@@ -87,31 +87,8 @@ var (
 	_ = 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)
+	i = 4
+	j = 1
+	_ = time.Date(2023+i, 1+i, 2*i, i+j, i-j, 0, 0, time.UTC)
 )
diff --git a/testdata/time_date_out_of_bounds.go b/testdata/time_date_out_of_bounds.go
new file mode 100644
index 0000000..74f59bb
--- /dev/null
+++ b/testdata/time_date_out_of_bounds.go
@@ -0,0 +1,146 @@
+package pkg
+
+import "time"
+
+// these notations are supported by Go, the rule reports them anyway
+var (
+	// The month argument is 0 considered as January by Go, but the rule reports it anyway
+	_ = time.Date(2023, 0, 2, 3, 4, 5, 6, time.UTC) // MATCH /time.Date month argument should not be zero/
+
+	// The day argument is 0 considered as the first day of the month by Go, but the rule reports it anyway
+	_ = time.Date(2023, 1, 0, 3, 4, 5, 6, time.UTC) // MATCH /time.Date day argument should not be zero/
+
+	// there is no need to use a plus sign in front of numbers
+	_ = time.Date(
+		+2023, // MATCH /time.Date year argument contains a useless plus sign: +2023/
+		+0b1,  // MATCH /time.Date month argument contains a useless plus sign: +0b1/
+		+02,   // MATCH /time.Date day argument contains a useless plus sign: +02/
+		+0o3,  // MATCH /time.Date hour argument contains a useless plus sign: +0o3/
+		+4,    // MATCH /time.Date minute argument contains a useless plus sign: +4/
+		+5,    // MATCH /time.Date second argument contains a useless plus sign: +5/
+		+6,    // MATCH /time.Date nanosecond argument contains a useless plus sign: +6/
+		time.UTC)
+
+	// time.Date supports negative values, but it's uncommon to use them.
+	// the rule reports them
+	_ = time.Date(
+		2023,
+		-0b1, // MATCH /time.Date month argument is negative: -0b1/
+		-02,  // MATCH /time.Date day argument is negative: -02/
+		-0o3, // MATCH /time.Date hour argument is negative: -0o3/
+		-0x4, // MATCH /time.Date minute argument is negative: -0x4/
+		-5,   // MATCH /time.Date second argument is negative: -5/
+		-6,   // MATCH /time.Date nanosecond argument is negative: -6/
+		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) // MATCH /time.Date nanosecond argument should be between 0 and 999999999: 1000000000000000000/
+
+)
+
+// Common user errors with dates
+var (
+	// June has only 30 days, so there is no June 31st
+	_ = time.Date(2023, 6, 31, 3, 4, 5, 0, time.UTC) // MATCH /time.Date day argument is 31, but June has only 30 days/
+
+	// there is no January 32nd
+	_ = time.Date(2024, 1, 32, 3, 4, 5, 0, time.UTC) // MATCH /time.Date day argument is 32, but January has only 31 days/
+
+	// there is no February 29th in 2023 (not a leap year)
+	_ = time.Date(2023, 2, 29, 3, 4, 5, 0, time.UTC) // MATCH /time.Date day argument is 29, but February 2023 has only 28 days/
+
+	// there is no February 30th
+	_ = time.Date(2024, 2, 30, 3, 4, 5, 0, time.UTC) // MATCH /time.Date day argument is 30, but February 2024 has only 29 days/
+)
+
+// day and month arguments are swapped
+var (
+	_ = time.Date(2023, 30, 6, 3, 4, 5, 0, time.UTC)
+	// MATCH:59 /time.Date month argument should be between 1 and 12: 30/
+	// MATCH:59 /time.Date month and day arguments appear to be swapped: 2023-06-30 vs 2023-30-06/
+
+	// month and day arguments are swapped, but there is no June 31st
+	_ = time.Date(2023, 31, 6, 3, 4, 5, 0, time.UTC) // MATCH /time.Date month argument should be between 1 and 12: 31/
+)
+
+// edge cases to validate the order of the checks
+var (
+
+	// here the month argument could have been swapped with the day argument
+	// but the day argument is zero
+	_ = time.Date(2023, 31, 0, 3, 4, 5, 0, time.UTC)
+	// MATCH:72 /time.Date day argument should not be zero/
+	// MATCH:72 /time.Date month argument should be between 1 and 12: 31/
+
+	// here the month argument could have been swapped with the day argument
+	// but the day argument is negative
+	_ = time.Date(2023, 31, -1, 3, 4, 5, 0, time.UTC)
+	// MATCH:78 /time.Date day argument is negative: -1/
+	// MATCH:78 /time.Date month argument should be between 1 and 12: 31/
+)
+
+// arguments are totally out of bounds
+var (
+	_ = time.Date(
+		2023,
+		13, // MATCH /time.Date month argument should be between 1 and 12: 13/
+		// here we cannot detect the number of days in the month, so it falls back to the default of 31
+		32,         // MATCH /time.Date day argument should be between 1 and 31: 32/
+		25,         // MATCH /time.Date hour argument should be between 0 and 23: 25/
+		60,         // MATCH /time.Date minute argument should be between 0 and 59: 60/
+		61,         // MATCH /time.Date second argument should be between 0 and 60: 61/
+		1000000000, // MATCH /time.Date nanosecond argument should be between 0 and 999999999: 1000000000/
+		time.UTC)
+)
+
+// valid time.Date calls that should not be reported by the rule
+// there are provided to prevent regression.
+var (
+	// a date before the start of era is valid
+	_ = time.Date(-500, 1, 2, 3, 4, 5, 6, time.UTC)
+
+	// a date in the future is valid
+	_ = time.Date(3000, 1, 2, 3, 4, 5, 6, time.UTC)
+
+	// 0, 1 and, -1 are valid and distinct years in Go
+	_ = time.Date(0, 1, 2, 3, 4, 5, 6, time.UTC)
+	_ = time.Date(-1, 1, 2, 3, 4, 5, 6, time.UTC)
+	_ = time.Date(1, 1, 2, 3, 4, 5, 6, time.UTC)
+
+	// midnight is a valid time
+	_ = time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC)
+
+	// 2023 is not a leap year, so February has only 28 days
+	_ = time.Date(2023, 2, 28, 3, 4, 5, 0, time.UTC)
+
+	// 2024 is a leap year, so February has 29 days
+	_ = time.Date(2024, 2, 29, 3, 4, 5, 0, time.UTC)
+
+	// 2020 is not a leap year, so February has only 28 days
+	_ = time.Date(2020, 2, 28, 3, 4, 5, 0, time.UTC)
+
+	// a leap second is valid
+	_ = time.Date(2016, 12, 31, 23, 59, 60, 0, time.UTC)
+)
+
+// these should not be reported by the rule
+var (
+	a int
+	_ = time.Date(2023, 1, a, 3, 4, 5, 6, time.UTC)
+	_ = time.Date(2023, 1, -a, 3, 4, 5, 6, time.UTC)
+	_ = time.Date(2023, 1, +a, 3, 4, 5, 6, time.UTC)
+	_ = time.Date(2023, 1, ^1, 3, 4, 5, 6, time.UTC)
+	_ = time.Date(2023, 1, ^a, 3, 4, 5, 6, time.UTC)
+
+	// obscure, but valid, notations that are ignored
+	_ = time.Date(
+		+-2023,
+		-+1,
+		+-+2,
+		-+-3,
+		+-+-4,
+		-+-+5,
+		+-+-+6,
+		time.UTC)
+)