Repository: mgechev/revive
Pull request #1370: feature: detect time/date out of bounds in time-date rule
Author: login: ccoVeille
Created: 2025-05-22T21:07:35Z | Merged: 2025-05-27T10:25:01Z

<!-- ### 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

Linked issue #1369: 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.

Linked issue comments: user: login: ccoVeille
created_at: 2025-05-23T06:01:47Z
body: For records, I also raised an idea in usestdlibvars to use time.Month constants (time.January, …)

- https://github.com/sashamelentyev/usestdlibvars/issues/103

I opened it there because their linter is already suggesting using time.Month constants, and is the right candidate one.

Let's see if they are interested. Otherwise, the idea could go back here.
author_association: CONTRIBUTOR

Diff:
diff --git a/RULES_DESCRIPTIONS.md b/RULES_DESCRIPTIONS.md
index b26d94fe8..943dc6866 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 ebcb083fb..939ea825c 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
 }
diff --git a/test/time_date_test.go b/test/time_date_test.go
index c379a9d68..eb4ba6df7 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 f63ad39a9..8b82fbea7 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 000000000..74f59bba8
--- /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)
+)


Reviews:
user: login: ccoVeille
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T21:08:57Z
user: login: chavacava
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T05:34:47Z
user: login: chavacava
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T05:42:49Z
user: login: chavacava
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T05:46:15Z
user: login: ccoVeille
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T05:52:00Z
user: login: ccoVeille
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-25T14:40:43Z
user: login: alexandear
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-26T18:31:12Z
user: login: ccoVeille
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T19:32:05Z
user: login: chavacava
body: I've some remarks/questions but LGTM
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-26T19:42:02Z
user: login: ccoVeille
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T19:57:52Z
user: login: ccoVeille
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T20:02:14Z
user: login: ccoVeille
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T20:04:55Z
user: login: ccoVeille
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T21:58:37Z
user: login: chavacava
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-27T05:39:02Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

Adds validation for out-of-bounds values in calls to `time.Date`, ensuring month, day, hour, minute, second, and nanosecond arguments fall within their legal ranges.

- Introduces a new test file (`time_date_out_of_bounds.go`) with cases covering zero, negative, swapped, and extreme values.
- Updates the `TimeDateRule` implementation to track each argument’s boundaries, refactors literal parsing to return `int64`, and adds unary-sign handling.
- Adjusts existing decimal‐literal tests and updates documentation/examples in `RULES_DESCRIPTIONS.md`.

### Reviewed Changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                                | Description                                           |
|-------------------------------------|-------------------------------------------------------|
| testdata/time_date_out_of_bounds.go | New var block with test cases for out‐of‐bounds dates |
| testdata/time_date_decimal_literal.go | Replaces old examples with an expression case using `i` and `j` |
| test/time_date_test.go              | Registers the new out‐of‐bounds test suite            |
| rule/time_date.go                   | Implements argument boundary checks and refactors parsing |
| RULES_DESCRIPTIONS.md               | Expands rule examples and adds detailed `<details>`   |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**testdata/time_date_decimal_literal.go:91**
* The assignment `i = 4` is invalid at package scope inside a var block. You need to declare these as `var i = 4` (and `var j = 1`) or move them into a function scope.
```
i = 4
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T09:15:39Z
user: login: alexandear
body: LGTM

Left a few minor comments.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-27T09:22:34Z

Review comments:
diff_hunk: @@ -66,14 +75,45 @@ func (w lintTimeDate) Visit(n ast.Node) ast.Visitor {
 	}
 
 	// 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] {
+	for pos, realArg := range ce.Args[:timeDateArity-1] {
+
+		arg := realArg
+		var isNegative bool
+		ua, found := arg.(*ast.UnaryExpr)
+		if found {
+			// this is the argument we want to check
+			arg = ua.X
+			if ua.Op == token.SUB {
+				isNegative = true
+			}
+		}
+
 		bl, ok := arg.(*ast.BasicLit)
 		if !ok {
 			continue
 		}
+		fieldName := timeDateArgumentNames[pos]
 
-		replacedValue, err := parseDecimalInteger(bl)
+		parsedValue, err := parseDecimalInteger(bl)
 		if err == nil {
+			if isNegative {
+				parsedValue = -parsedValue
+			}
+
+			boundaries, found := timeDateArgumentBoundaries[fieldName]
+			if found {
+				if parsedValue < boundaries[0] || parsedValue > boundaries[1] {
+					w.onFailure(lint.Failure{
+						Category:   "time",
+						Node:       realArg,
+						Confidence: 0.8,
+						Failure: fmt.Sprintf(
+							"time.Date %s argument is supposed to be between %d and %d, found: %d",
path: rule/time_date.go
user: login: ccoVeille
body: Note for myself to resolve, the README will have to be updated
created_at: 2025-05-22T21:08:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,53 @@
+package pkg
+
+import "time"
+
+// these notations are supported by Go, the rule reports them anyway
+var (
+	// negative values can be used
+	// Go will compute 2022-10-28 20:55:54.999999994 +0000 UTC
+	_ = time.Date(
+		2023,
+		-1, // MATCH /time.Date month argument is supposed to be between 1 and 12, found: -1/
+		-2, // MATCH /time.Date day argument is supposed to be between 1 and 31, found: -2/
+		-3, // MATCH /time.Date hour argument is supposed to be between 0 and 23, found: -3/
+		-4, // MATCH /time.Date minute argument is supposed to be between 0 and 59, found: -4/
+		-5, // MATCH /time.Date second argument is supposed to be between 0 and 59, found: -5/
+		-6, // MATCH /time.Date nanosecond argument is supposed to be between 0 and 999999999, found: -6/
+		time.UTC)
+
+	_ = time.Date(
+		2023,
+		0, // MATCH /time.Date month argument is supposed to be between 1 and 12, found: 0/
+		0, // MATCH /time.Date day argument is supposed to be between 1 and 31, found: 0/
+		0,
+		0,
+		0,
+		0,
+		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, // MATCH /time.Date month argument is supposed to be between 1 and 12, found: 13/
+		32, // MATCH /time.Date day argument is supposed to be between 1 and 31, found: 32/
+		27, // MATCH /time.Date hour argument is supposed to be between 0 and 23, found: 27/
+		70, // MATCH /time.Date minute argument is supposed to be between 0 and 59, found: 70/
+		70, // MATCH /time.Date second argument is supposed to be between 0 and 59, found: 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) // MATCH /time.Date nanosecond argument is supposed to be between 0 and 999999999, found: 1000000000000000000/
+)
+
+// 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)
path: testdata/time_date_out_of_bounds.go
user: login: chavacava
body: We could check days vs month. Except for February, checks are trivial. 
For February: 29 is "tricky" while 30 and 31 are easy.
created_at: 2025-05-23T05:34:47Z
author_association: COLLABORATOR
diff_hunk: @@ -66,14 +75,44 @@ func (w lintTimeDate) Visit(n ast.Node) ast.Visitor {
 	}
 
 	// 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] {
+	for pos, realArg := range ce.Args[:timeDateArity-1] {
+		arg := realArg
+		var isNegative bool
+		ua, found := arg.(*ast.UnaryExpr)
+		if found {
+			// this is the argument we want to check
+			arg = ua.X
+			if ua.Op == token.SUB {
+				isNegative = true
+			}
+		}
+
 		bl, ok := arg.(*ast.BasicLit)
 		if !ok {
 			continue
 		}
+		fieldName := timeDateArgumentNames[pos]
 
-		replacedValue, err := parseDecimalInteger(bl)
+		parsedValue, err := parseDecimalInteger(bl)
 		if err == nil {
+			if isNegative {
+				parsedValue = -parsedValue
+			}
+
+			boundaries, found := timeDateArgumentBoundaries[fieldName]
+			if found {
+				if parsedValue < boundaries[0] || parsedValue > boundaries[1] {
path: rule/time_date.go
user: login: chavacava
body: ```suggestion
				min := boundaries[0]
				max := boundaries[1]
				if parsedValue < min || parsedValue > max {
```

(then in the failure msg we can also use `min` and `max`)
created_at: 2025-05-23T05:42:49Z
author_association: COLLABORATOR
diff_hunk: @@ -66,14 +75,44 @@ func (w lintTimeDate) Visit(n ast.Node) ast.Visitor {
 	}
 
 	// The last argument is a timezone, there is no need to check it, also it has a different type
path: rule/time_date.go
user: login: chavacava
body: We could check if the timezone is not `nil` (will panic)
(almost nobody uses nil for the timezone but ...)
created_at: 2025-05-23T05:46:15Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,53 @@
+package pkg
+
+import "time"
+
+// these notations are supported by Go, the rule reports them anyway
+var (
+	// negative values can be used
+	// Go will compute 2022-10-28 20:55:54.999999994 +0000 UTC
+	_ = time.Date(
+		2023,
+		-1, // MATCH /time.Date month argument is supposed to be between 1 and 12, found: -1/
+		-2, // MATCH /time.Date day argument is supposed to be between 1 and 31, found: -2/
+		-3, // MATCH /time.Date hour argument is supposed to be between 0 and 23, found: -3/
+		-4, // MATCH /time.Date minute argument is supposed to be between 0 and 59, found: -4/
+		-5, // MATCH /time.Date second argument is supposed to be between 0 and 59, found: -5/
+		-6, // MATCH /time.Date nanosecond argument is supposed to be between 0 and 999999999, found: -6/
+		time.UTC)
+
+	_ = time.Date(
+		2023,
+		0, // MATCH /time.Date month argument is supposed to be between 1 and 12, found: 0/
+		0, // MATCH /time.Date day argument is supposed to be between 1 and 31, found: 0/
+		0,
+		0,
+		0,
+		0,
+		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, // MATCH /time.Date month argument is supposed to be between 1 and 12, found: 13/
+		32, // MATCH /time.Date day argument is supposed to be between 1 and 31, found: 32/
+		27, // MATCH /time.Date hour argument is supposed to be between 0 and 23, found: 27/
+		70, // MATCH /time.Date minute argument is supposed to be between 0 and 59, found: 70/
+		70, // MATCH /time.Date second argument is supposed to be between 0 and 59, found: 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) // MATCH /time.Date nanosecond argument is supposed to be between 0 and 999999999, found: 1000000000000000000/
+)
+
+// 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)
path: testdata/time_date_out_of_bounds.go
user: login: ccoVeille
body: I coded it locally yesterday, it works well. But I dislike how my code is structured, I'll have to refactor it.

I wanted to get feedbacks on the PR idea, first.

It seems OK, I'll refactor then.
created_at: 2025-05-23T05:52:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -66,14 +75,44 @@ func (w lintTimeDate) Visit(n ast.Node) ast.Visitor {
 	}
 
 	// The last argument is a timezone, there is no need to check it, also it has a different type
path: rule/time_date.go
user: login: ccoVeille
body: I opened https://github.com/mgechev/revive/pull/1377 about it

- https://github.com/mgechev/revive/pull/1377
created_at: 2025-05-25T14:40:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,6 +327,30 @@ func (w lintTimeDate) Visit(n ast.Node) ast.Visitor {
 	return w
 }
 
+// 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 {
+	// here 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) getNumberOfDaysInMonth(year int64, month time.Month) int64 {
path: rule/time_date.go
user: login: alexandear
body: can be simply:
```suggestion
func (w lintTimeDate) daysInMonth(year int64, month time.Month) int64 {
```
created_at: 2025-05-26T18:30:45Z
author_association: COLLABORATOR
diff_hunk: @@ -77,15 +92,179 @@ 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] {
+		fieldName := timeDateArgumentNames[pos]
+
+		providedValue := astutils.GoFmt(arg)
path: rule/time_date.go
user: login: chavacava
body: Move these variable declarations near their respective uses
created_at: 2025-05-26T19:10:09Z
author_association: COLLABORATOR
diff_hunk: @@ -77,15 +92,179 @@ 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] {
+		fieldName := timeDateArgumentNames[pos]
+
+		providedValue := astutils.GoFmt(arg)
+
+		var hasPositiveSign, hasNegativeSign bool
+		if node, ok := arg.(*ast.UnaryExpr); ok {
+			// we can have a unary expression like -1, +1, -0x1234, +0b101010
+			switch node.Op {
+			case token.SUB:
+				// this is a negative number
+				hasNegativeSign = true
+			case token.ADD:
+				// there is a positive sign, but it is not necessary
+				hasPositiveSign = true
+			default:
+				// this is unexpected, we can ignore it
+				return nil
+			}
+
+			arg = node.X
+		}
+
 		bl, ok := arg.(*ast.BasicLit)
 		if !ok {
-			continue
+			return nil
 		}
 
-		replacedValue, err := parseDecimalInteger(bl)
+		parsedValue, err := parseDecimalInteger(bl)
 		if err == nil {
+			if hasPositiveSign {
+				// it is not necessary to have a positive sign
+				// we can still report it as a failure, but with lower confidence
+				w.onFailure(lint.Failure{
+					Category:   "time",
+					Node:       arg,
+					Confidence: 0.8,
+					Failure: fmt.Sprintf(
+						"time.Date %s argument contains an useless plus sign: %s",
+						fieldName, providedValue,
+					),
+				})
+			}
+
+			if fieldName == "year" {
+				// 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
+			}
+
+			if hasNegativeSign {
+				w.onFailure(lint.Failure{
+					Category:   "time",
+					Node:       arg,
+					Confidence: 0.5,
+					Failure: fmt.Sprintf(
+						"time.Date %s argument is negative: %s",
+						fieldName, providedValue,
+					),
+				})
+				continue
+			}
+
+			boundaries, ok := timeDateArgumentBoundaries[fieldName]
+			if !ok {
+				// no boundaries for this field, skip it
+				continue
+			}
+			minValue, maxValue := boundaries[0], boundaries[1]
+
+			switch fieldName {
+			case "month":
+				parsedDate.month = parsedValue
+
+				if parsedValue == 0 {
+					// this is a special case, where the month is 0
+					// Go will ignore it and use January
+					// but we can still report it as a failure
+
+					w.onFailure(lint.Failure{
+						Category:   "time",
+						Node:       arg,
+						Confidence: 0.8,
path: rule/time_date.go
user: login: chavacava
body: Shouldn´t confidence be 1 here? Because we are 100% confident that the value is 0. 

Same for the case of day
created_at: 2025-05-26T19:14:54Z
author_association: COLLABORATOR
diff_hunk: @@ -77,15 +92,179 @@ 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] {
+		fieldName := timeDateArgumentNames[pos]
+
+		providedValue := astutils.GoFmt(arg)
+
+		var hasPositiveSign, hasNegativeSign bool
+		if node, ok := arg.(*ast.UnaryExpr); ok {
+			// we can have a unary expression like -1, +1, -0x1234, +0b101010
+			switch node.Op {
+			case token.SUB:
+				// this is a negative number
+				hasNegativeSign = true
+			case token.ADD:
+				// there is a positive sign, but it is not necessary
+				hasPositiveSign = true
+			default:
+				// this is unexpected, we can ignore it
+				return nil
+			}
+
+			arg = node.X
+		}
+
 		bl, ok := arg.(*ast.BasicLit)
 		if !ok {
-			continue
+			return nil
 		}
 
-		replacedValue, err := parseDecimalInteger(bl)
+		parsedValue, err := parseDecimalInteger(bl)
 		if err == nil {
+			if hasPositiveSign {
+				// it is not necessary to have a positive sign
+				// we can still report it as a failure, but with lower confidence
+				w.onFailure(lint.Failure{
path: rule/time_date.go
user: login: chavacava
body: Why fail on + (or -) sign just after parsing and not before ? Why do not generate a failure before (in lines 107 and 110)?


created_at: 2025-05-26T19:19:53Z
author_association: COLLABORATOR
diff_hunk: @@ -77,15 +92,179 @@ 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] {
+		fieldName := timeDateArgumentNames[pos]
+
+		providedValue := astutils.GoFmt(arg)
+
+		var hasPositiveSign, hasNegativeSign bool
+		if node, ok := arg.(*ast.UnaryExpr); ok {
+			// we can have a unary expression like -1, +1, -0x1234, +0b101010
+			switch node.Op {
+			case token.SUB:
+				// this is a negative number
+				hasNegativeSign = true
+			case token.ADD:
+				// there is a positive sign, but it is not necessary
+				hasPositiveSign = true
+			default:
+				// this is unexpected, we can ignore it
+				return nil
+			}
+
+			arg = node.X
+		}
path: rule/time_date.go
user: login: chavacava
body: This can be done in a helper function. Something like
```suggestion
func checkForSigns(arg *ast.Expr, onFailure ...) *ast.Expr {
  if node, ok := arg.(*ast.UnaryExpr); ok {
	// we can have a unary expression like -1, +1, -0x1234, +0b101010
	switch node.Op {
	case token.SUB:
		// this is a negative number
		// generate a failure 
		arg = node.X
	case token.ADD:
		// there is a positive sign, but it is not necessary
		// generate a failure 
		arg = nil
	default:
		// this is unexpected, we can ignore it
		arg = nil
        }
  }

  return arg
}
```

Then at call site
```go
arg = checkForSigns( ...) 
if arg = nil {
  return nil
}
```
created_at: 2025-05-26T19:28:53Z
author_association: COLLABORATOR
diff_hunk: @@ -77,15 +92,179 @@ 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] {
+		fieldName := timeDateArgumentNames[pos]
+
+		providedValue := astutils.GoFmt(arg)
+
+		var hasPositiveSign, hasNegativeSign bool
+		if node, ok := arg.(*ast.UnaryExpr); ok {
+			// we can have a unary expression like -1, +1, -0x1234, +0b101010
+			switch node.Op {
+			case token.SUB:
+				// this is a negative number
+				hasNegativeSign = true
+			case token.ADD:
+				// there is a positive sign, but it is not necessary
+				hasPositiveSign = true
+			default:
+				// this is unexpected, we can ignore it
+				return nil
+			}
+
+			arg = node.X
+		}
+
 		bl, ok := arg.(*ast.BasicLit)
 		if !ok {
-			continue
+			return nil
 		}
 
-		replacedValue, err := parseDecimalInteger(bl)
+		parsedValue, err := parseDecimalInteger(bl)
 		if err == nil {
+			if hasPositiveSign {
+				// it is not necessary to have a positive sign
+				// we can still report it as a failure, but with lower confidence
+				w.onFailure(lint.Failure{
+					Category:   "time",
+					Node:       arg,
+					Confidence: 0.8,
+					Failure: fmt.Sprintf(
+						"time.Date %s argument contains an useless plus sign: %s",
+						fieldName, providedValue,
+					),
+				})
+			}
+
+			if fieldName == "year" {
+				// 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
+			}
+
+			if hasNegativeSign {
+				w.onFailure(lint.Failure{
+					Category:   "time",
+					Node:       arg,
+					Confidence: 0.5,
+					Failure: fmt.Sprintf(
+						"time.Date %s argument is negative: %s",
+						fieldName, providedValue,
+					),
+				})
+				continue
+			}
+
+			boundaries, ok := timeDateArgumentBoundaries[fieldName]
+			if !ok {
+				// no boundaries for this field, skip it
+				continue
+			}
+			minValue, maxValue := boundaries[0], boundaries[1]
+
+			switch fieldName {
+			case "month":
+				parsedDate.month = parsedValue
+
+				if parsedValue == 0 {
+					// this is a special case, where the month is 0
+					// Go will ignore it and use January
+					// but we can still report it as a failure
+
+					w.onFailure(lint.Failure{
+						Category:   "time",
+						Node:       arg,
+						Confidence: 0.8,
+						Failure:    "time.Date month argument should not be zero",
+					})
+					continue
+				}
+
+			case "day":
+
+				switch {
+				case parsedValue == 0:
+					// this is a special case, where the day is 0
+					// Go will ignore it and use the first day of the month
+					// but we can still report it as a failure
+
+					w.onFailure(lint.Failure{
+						Category:   "time",
+						Node:       arg,
+						Confidence: 0.8,
+						Failure:    "time.Date day argument should not be zero",
+					})
+					continue
+
+				// the month is valid, we can check the day
+				case parsedDate.month >= 1 && parsedDate.month <= 12:
+					// the month is valid, we can check the day
+
+					month := time.Month(parsedDate.month)
+
+					maxValue = w.daysInMonth(parsedDate.year, month)
+
+					var periodInfo string
+					if month == time.February {
+						periodInfo += " " + 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, month.String()+periodInfo, maxValue,
+							),
+						})
+						continue
+					}
+
+				// possible day and month swap
+				case parsedDate.month > 12 && parsedDate.month <= 31 && parsedValue <= 12:
+					// this is a special case, where the month and day arguments are swapped
+					// for example: time.Date(2023, 31, 6, 0, 0, 0, 0, time.UTC)
+
+					realMonth, realDay := parsedValue, parsedDate.month
+
+					// we must ensure that the date is valid
+					maxValue = w.daysInMonth(parsedDate.year, time.Month(realMonth))
+
+					if realDay >= 1 && realDay <= maxValue {
path: rule/time_date.go
user: login: ccoVeille
body: self review without a laptop

FIXME: define a new variable and use it

Otherwise, the check with max value can be invalid in the following if statement could be invalid.

TODO: add a test about it

2023-29-2
2023-31-6

I feel like it falls through the net otherwise
created_at: 2025-05-26T19:31:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,15 +92,179 @@ 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] {
+		fieldName := timeDateArgumentNames[pos]
+
+		providedValue := astutils.GoFmt(arg)
path: rule/time_date.go
user: login: ccoVeille
body: I cannot.

`arg` is replaced with the unary test.

If I move it. The sign (+/-) would be removed
created_at: 2025-05-26T19:57:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,15 +92,179 @@ 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] {
+		fieldName := timeDateArgumentNames[pos]
+
+		providedValue := astutils.GoFmt(arg)
+
+		var hasPositiveSign, hasNegativeSign bool
+		if node, ok := arg.(*ast.UnaryExpr); ok {
+			// we can have a unary expression like -1, +1, -0x1234, +0b101010
+			switch node.Op {
+			case token.SUB:
+				// this is a negative number
+				hasNegativeSign = true
+			case token.ADD:
+				// there is a positive sign, but it is not necessary
+				hasPositiveSign = true
+			default:
+				// this is unexpected, we can ignore it
+				return nil
+			}
+
+			arg = node.X
+		}
+
 		bl, ok := arg.(*ast.BasicLit)
 		if !ok {
-			continue
+			return nil
 		}
 
-		replacedValue, err := parseDecimalInteger(bl)
+		parsedValue, err := parseDecimalInteger(bl)
 		if err == nil {
+			if hasPositiveSign {
+				// it is not necessary to have a positive sign
+				// we can still report it as a failure, but with lower confidence
+				w.onFailure(lint.Failure{
path: rule/time_date.go
user: login: ccoVeille
body: Because the code could be

```go
var a int
time.Date(2024, +a, 1...)
time.Date(2024, +0o1, 1...)
time.Date(2024, -a, 1...)
time.Date(2024, -0o1, 1...)
```

I need to make sure the minus is with a decimal value I can parse

I'll add tests about it, and also comments.
created_at: 2025-05-26T20:02:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,15 +92,179 @@ 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] {
+		fieldName := timeDateArgumentNames[pos]
+
+		providedValue := astutils.GoFmt(arg)
+
+		var hasPositiveSign, hasNegativeSign bool
+		if node, ok := arg.(*ast.UnaryExpr); ok {
+			// we can have a unary expression like -1, +1, -0x1234, +0b101010
+			switch node.Op {
+			case token.SUB:
+				// this is a negative number
+				hasNegativeSign = true
+			case token.ADD:
+				// there is a positive sign, but it is not necessary
+				hasPositiveSign = true
+			default:
+				// this is unexpected, we can ignore it
+				return nil
+			}
+
+			arg = node.X
+		}
path: rule/time_date.go
user: login: ccoVeille
body: See https://github.com/mgechev/revive/pull/1370#discussion_r2107811347

But maybe I would be able to use it somehow, with light changes
created_at: 2025-05-26T20:04:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,15 +92,179 @@ 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] {
+		fieldName := timeDateArgumentNames[pos]
+
+		providedValue := astutils.GoFmt(arg)
path: rule/time_date.go
user: login: ccoVeille
body: I refactored
created_at: 2025-05-26T21:57:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,15 +92,179 @@ 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] {
+		fieldName := timeDateArgumentNames[pos]
+
+		providedValue := astutils.GoFmt(arg)
+
+		var hasPositiveSign, hasNegativeSign bool
+		if node, ok := arg.(*ast.UnaryExpr); ok {
+			// we can have a unary expression like -1, +1, -0x1234, +0b101010
+			switch node.Op {
+			case token.SUB:
+				// this is a negative number
+				hasNegativeSign = true
+			case token.ADD:
+				// there is a positive sign, but it is not necessary
+				hasPositiveSign = true
+			default:
+				// this is unexpected, we can ignore it
+				return nil
+			}
+
+			arg = node.X
+		}
+
 		bl, ok := arg.(*ast.BasicLit)
 		if !ok {
-			continue
+			return nil
 		}
 
-		replacedValue, err := parseDecimalInteger(bl)
+		parsedValue, err := parseDecimalInteger(bl)
 		if err == nil {
+			if hasPositiveSign {
+				// it is not necessary to have a positive sign
+				// we can still report it as a failure, but with lower confidence
+				w.onFailure(lint.Failure{
+					Category:   "time",
+					Node:       arg,
+					Confidence: 0.8,
+					Failure: fmt.Sprintf(
+						"time.Date %s argument contains an useless plus sign: %s",
+						fieldName, providedValue,
+					),
+				})
+			}
+
+			if fieldName == "year" {
+				// 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
+			}
+
+			if hasNegativeSign {
+				w.onFailure(lint.Failure{
+					Category:   "time",
+					Node:       arg,
+					Confidence: 0.5,
+					Failure: fmt.Sprintf(
+						"time.Date %s argument is negative: %s",
+						fieldName, providedValue,
+					),
+				})
+				continue
+			}
+
+			boundaries, ok := timeDateArgumentBoundaries[fieldName]
+			if !ok {
+				// no boundaries for this field, skip it
+				continue
+			}
+			minValue, maxValue := boundaries[0], boundaries[1]
+
+			switch fieldName {
+			case "month":
+				parsedDate.month = parsedValue
+
+				if parsedValue == 0 {
+					// this is a special case, where the month is 0
+					// Go will ignore it and use January
+					// but we can still report it as a failure
+
+					w.onFailure(lint.Failure{
+						Category:   "time",
+						Node:       arg,
+						Confidence: 0.8,
path: rule/time_date.go
user: login: ccoVeille
body: indeed
created_at: 2025-05-26T21:57:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,15 +92,179 @@ 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] {
+		fieldName := timeDateArgumentNames[pos]
+
+		providedValue := astutils.GoFmt(arg)
+
+		var hasPositiveSign, hasNegativeSign bool
+		if node, ok := arg.(*ast.UnaryExpr); ok {
+			// we can have a unary expression like -1, +1, -0x1234, +0b101010
+			switch node.Op {
+			case token.SUB:
+				// this is a negative number
+				hasNegativeSign = true
+			case token.ADD:
+				// there is a positive sign, but it is not necessary
+				hasPositiveSign = true
+			default:
+				// this is unexpected, we can ignore it
+				return nil
+			}
+
+			arg = node.X
+		}
+
 		bl, ok := arg.(*ast.BasicLit)
 		if !ok {
-			continue
+			return nil
 		}
 
-		replacedValue, err := parseDecimalInteger(bl)
+		parsedValue, err := parseDecimalInteger(bl)
 		if err == nil {
+			if hasPositiveSign {
+				// it is not necessary to have a positive sign
+				// we can still report it as a failure, but with lower confidence
+				w.onFailure(lint.Failure{
path: rule/time_date.go
user: login: ccoVeille
body: I have added the test and refactored
created_at: 2025-05-26T21:57:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,15 +92,179 @@ 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] {
+		fieldName := timeDateArgumentNames[pos]
+
+		providedValue := astutils.GoFmt(arg)
+
+		var hasPositiveSign, hasNegativeSign bool
+		if node, ok := arg.(*ast.UnaryExpr); ok {
+			// we can have a unary expression like -1, +1, -0x1234, +0b101010
+			switch node.Op {
+			case token.SUB:
+				// this is a negative number
+				hasNegativeSign = true
+			case token.ADD:
+				// there is a positive sign, but it is not necessary
+				hasPositiveSign = true
+			default:
+				// this is unexpected, we can ignore it
+				return nil
+			}
+
+			arg = node.X
+		}
path: rule/time_date.go
user: login: ccoVeille
body: I added this method and adapted a bit
created_at: 2025-05-26T21:58:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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 used are used as arguments
path: RULES_DESCRIPTIONS.md
user: login: alexandear
body: ```suggestion
- Non-decimal integers are used as arguments
```
created_at: 2025-05-27T09:11:15Z
author_association: COLLABORATOR
diff_hunk: @@ -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 used are used as arguments
path: RULES_DESCRIPTIONS.md
user: login: Copilot
body: [nitpick] There's a duplicated word 'used'. Consider changing this line to: `- Non-decimal integers are used as arguments`.
```suggestion
- Non-decimal integers are used as arguments
```
created_at: 2025-05-27T09:15:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -77,15 +106,131 @@ 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 {
+					// this is a special case, where the month is 0
+					// Go will ignore it and use January,
+					// but we can 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:
+					// this is a special case, where the day is 0
+					// Go will ignore it and use the first day of the month,
+					// but we can still report it as a failure
+
path: rule/time_date.go
user: login: alexandear
body: ```suggestion
					// Special case: day is 0.
					// Go treats it as the first day of the month, but we still report it as a failure.
```
created_at: 2025-05-27T09:18:15Z
author_association: COLLABORATOR
diff_hunk: @@ -77,15 +106,131 @@ 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 {
+					// this is a special case, where the month is 0
+					// Go will ignore it and use January,
+					// but we can still report it as a failure
+
path: rule/time_date.go
user: login: alexandear
body: The comment could be more concise and shorter:

```suggestion
					// Special case: month is 0.
					// Go treats it as January, but we still report it as a failure.
```
created_at: 2025-05-27T09:18:50Z
author_association: COLLABORATOR
diff_hunk: @@ -77,15 +106,131 @@ 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 {
+					// this is a special case, where the month is 0
+					// Go will ignore it and use January,
+					// but we can 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:
+					// this is a special case, where the day is 0
+					// Go will ignore it and use the first day of the month,
+					// but we can 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, we can check the day
path: rule/time_date.go
user: login: alexandear
body: ```suggestion
				// the month is valid, check the day
```
created_at: 2025-05-27T09:19:09Z
author_association: COLLABORATOR
diff_hunk: @@ -140,13 +286,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
path: rule/time_date.go
user: login: alexandear
body: ```suggestion
		// Other unary expressions are not supported.
```
created_at: 2025-05-27T09:20:55Z
author_association: COLLABORATOR
diff_hunk: @@ -140,13 +286,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:
path: rule/time_date.go
user: login: alexandear
body: ```suggestion
		// Any other expression is not supported.
		// It could be something like this:
```
created_at: 2025-05-27T09:21:11Z
author_association: COLLABORATOR
diff_hunk: @@ -140,13 +286,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.
path: rule/time_date.go
user: login: alexandear
body: ```suggestion
// This is used to check if the date is valid according to Go implementation.
```
created_at: 2025-05-27T09:21:40Z
author_association: COLLABORATOR

PR comments:
user: login: ccoVeille
created_at: 2025-05-22T21:08:01Z
body: This a naive approach that may require some iterations.
author_association: CONTRIBUTOR
user: login: ccoVeille
created_at: 2025-05-23T05:54:01Z
body: Switching back to draft to code the suggestion made during the code review.

It can be reviewed anyway
author_association: CONTRIBUTOR
user: login: ccoVeille
created_at: 2025-05-25T16:15:36Z
body: I'll wait for the #1377 #1378 to be merged and reviewed before coming back to this PR

- #1377
- #1378
author_association: CONTRIBUTOR
user: login: ccoVeille
created_at: 2025-05-26T15:44:31Z
body: Here are the change I made since the last time I worked on it:

I'm now validating arguments with multiple rules:
- day and month equals to 0 are now reported differently
- the day argument checks for the number of day in a month, even for a leap year
- month and day inversion such as `time.Date(2023, 22, 6, ...)` when it should be `time.Date(2023, 6, 22, ...)`

There are some other changes, but they are the main ones.

Please review again, thanks

Let me know if you think I should adapt the confidence level of each error reporting.
author_association: CONTRIBUTOR
user: login: ccoVeille
created_at: 2025-05-26T22:04:25Z
body: I refactored code, it should be clearer I think, thanks @chavacava 
author_association: CONTRIBUTOR
user: login: ccoVeille
created_at: 2025-05-27T07:24:50Z
body: I feel like, I screw up my PR by resolving conflicts 😅
author_association: CONTRIBUTOR
user: login: ccoVeille
created_at: 2025-05-27T07:37:10Z
body: It can be reviewed again.
author_association: CONTRIBUTOR
user: login: ccoVeille
created_at: 2025-05-27T10:01:07Z
body: Thanks @alexandear I have applied the changes you suggested
author_association: CONTRIBUTOR