From 68b4ecddb007a00c9b426620ab5018ea325d6831 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Wed, 19 Aug 2026 10:16:51 +0200 Subject: [PATCH 1/4] Implement new rule S9346 Detect 32-bit or smaller integer values (int, short, byte, char) passed as arguments to timestamp-consuming APIs (Date, Timestamp, Instant, Calendar), where the narrow type causes overflow or data corruption. --- ...IntegerToLongTimestampCastCheckSample.java | 117 ++++++++++++++++++ .../IntegerToLongTimestampCastCheck.java | 100 +++++++++++++++ .../IntegerToLongTimestampCastCheckTest.java | 33 +++++ .../org/sonar/l10n/java/rules/java/S9346.html | 33 +++++ .../org/sonar/l10n/java/rules/java/S9346.json | 21 ++++ .../main/resources/profiles/Sonar_way/S9346 | 0 6 files changed, 304 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/java/checks/IntegerToLongTimestampCastCheckSample.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/IntegerToLongTimestampCastCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/IntegerToLongTimestampCastCheckTest.java create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9346.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9346.json create mode 100644 sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9346 diff --git a/java-checks-test-sources/default/src/main/java/checks/IntegerToLongTimestampCastCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/IntegerToLongTimestampCastCheckSample.java new file mode 100644 index 00000000000..44a458fb073 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/IntegerToLongTimestampCastCheckSample.java @@ -0,0 +1,117 @@ +package checks; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +class IntegerToLongTimestampCastCheckSample { + + static final int INT_CONSTANT = 1234567890; + + void noncompliantImplicitWidening() { + int intVar = 1000; + new Date(intVar); // Noncompliant {{Use a "long" value to represent this timestamp.}} +// ^^^^^^ + Instant.ofEpochSecond(intVar); // Noncompliant + Instant.ofEpochMilli(intVar); // Noncompliant + new Timestamp(intVar); // Noncompliant + } + + void noncompliantExplicitCast() { + int intVar = 1000; + new Date((long) intVar); // Noncompliant + Instant.ofEpochSecond((long) intVar); // Noncompliant + Instant.ofEpochMilli((long) intVar); // Noncompliant + new Timestamp((long) intVar); // Noncompliant + } + + void noncompliantArithmeticOverflow() { + int days = 365; + new Date((long) (days * 24 * 60 * 60 * 1000)); // Noncompliant + } + + void noncompliantCalendar() { + int intVar = 1000; + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(intVar); // Noncompliant + cal.setTimeInMillis((long) intVar); // Noncompliant + } + + void noncompliantGregorianCalendar() { + int intVar = 1000; + GregorianCalendar cal = new GregorianCalendar(); + cal.setTimeInMillis(intVar); // Noncompliant + } + + void noncompliantOtherNarrowTypes() { + short shortVar = 100; + byte byteVar = 10; + char charVar = 'A'; + new Date((long) shortVar); // Noncompliant + new Date((long) byteVar); // Noncompliant + new Date((long) charVar); // Noncompliant + } + + void noncompliantMethodReturn() { + new Date(getSeconds()); // Noncompliant + } + + void noncompliantIntConstant() { + Instant.ofEpochSecond(INT_CONSTANT); // Noncompliant + } + + void noncompliantOfEpochSecondTwoArgs() { + int intVar = 1000; + Instant.ofEpochSecond(intVar, 0L); // Noncompliant + } + + void noncompliantParenthesized() { + int intVar = 1000; + new Date((intVar)); // Noncompliant + } + + void noncompliantIntLiteral() { + new Date(0); // Noncompliant + } + + void compliantLongVariable() { + long longVar = 1234567890L; + new Date(longVar); + Instant.ofEpochSecond(longVar); + Instant.ofEpochMilli(longVar); + new Timestamp(longVar); + } + + void compliantCurrentTimeMillis() { + new Date(System.currentTimeMillis()); + } + + void compliantLongLiteral() { + new Date(1234567890L); + } + + void compliantCalendar() { + long longVar = 1234567890L; + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(longVar); + } + + void compliantLongMethodReturn() { + new Date(getMillis()); + } + + void compliantNonTimestampCast() { + int intVar = 1000; + long result = (long) intVar; + } + + int getSeconds() { + return 1000; + } + + long getMillis() { + return System.currentTimeMillis(); + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/IntegerToLongTimestampCastCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IntegerToLongTimestampCastCheck.java new file mode 100644 index 00000000000..2c407f10375 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/IntegerToLongTimestampCastCheck.java @@ -0,0 +1,100 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import org.sonar.check.Rule; +import org.sonar.java.checks.methods.AbstractMethodDetection; +import org.sonar.java.model.ExpressionUtils; +import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.NewClassTree; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.TypeCastTree; + +@Rule(key = "S9346") +public class IntegerToLongTimestampCastCheck extends AbstractMethodDetection { + + private static final String MESSAGE = "Use a \"long\" value to represent this timestamp."; + + private static final MethodMatchers CONSTRUCTOR_MATCHERS = MethodMatchers.or( + MethodMatchers.create() + .ofTypes("java.util.Date") + .constructor() + .addParametersMatcher("long") + .build(), + MethodMatchers.create() + .ofTypes("java.sql.Timestamp") + .constructor() + .addParametersMatcher("long") + .build()); + + private static final MethodMatchers METHOD_MATCHERS = MethodMatchers.or( + MethodMatchers.create() + .ofTypes("java.time.Instant") + .names("ofEpochSecond") + .addParametersMatcher("long") + .addParametersMatcher("long", "long") + .build(), + MethodMatchers.create() + .ofTypes("java.time.Instant") + .names("ofEpochMilli") + .addParametersMatcher("long") + .build(), + MethodMatchers.create() + .ofSubTypes("java.util.Calendar") + .names("setTimeInMillis") + .addParametersMatcher("long") + .build()); + + @Override + protected MethodMatchers getMethodInvocationMatchers() { + return MethodMatchers.or(CONSTRUCTOR_MATCHERS, METHOD_MATCHERS); + } + + @Override + protected void onMethodInvocationFound(MethodInvocationTree mit) { + checkArgument(mit.arguments().get(0)); + } + + @Override + protected void onConstructorFound(NewClassTree nct) { + checkArgument(nct.arguments().get(0)); + } + + private void checkArgument(ExpressionTree argument) { + ExpressionTree arg = ExpressionUtils.skipParentheses(argument); + if (arg.is(Tree.Kind.TYPE_CAST)) { + arg = ((TypeCastTree) arg).expression(); + } + Type type = arg.symbolType(); + if (type.isUnknown()) { + return; + } + if (isNarrowIntegerType(type)) { + reportIssue(argument, MESSAGE); + } + } + + private static boolean isNarrowIntegerType(Type type) { + return type.isPrimitive(Type.Primitives.INT) + || type.isPrimitive(Type.Primitives.SHORT) + || type.isPrimitive(Type.Primitives.BYTE) + || type.isPrimitive(Type.Primitives.CHAR); + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/IntegerToLongTimestampCastCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/IntegerToLongTimestampCastCheckTest.java new file mode 100644 index 00000000000..99dcf7ae75e --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/IntegerToLongTimestampCastCheckTest.java @@ -0,0 +1,33 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; + +class IntegerToLongTimestampCastCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/IntegerToLongTimestampCastCheckSample.java")) + .withCheck(new IntegerToLongTimestampCastCheck()) + .verifyIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9346.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9346.html new file mode 100644 index 00000000000..69a580697dd --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9346.html @@ -0,0 +1,33 @@ +

Using 32-bit signed integer types for timestamps can lead to serious reliability issues such as incorrect time representation, +system failures, and the Year 2038 problem.

+

Why is this an issue?

+

A 32-bit signed integer can hold values from -2,147,483,648 to 2,147,483,647. While this might seem like a large range, it's insufficient for +representing timestamps:

+ +

When you cast a 32-bit integer to a 64-bit integer for use as a timestamp, you're not fixing the underlying problem — the value is already +corrupted or limited by the 32-bit constraint before the cast happens.

+

Noncompliant code example

+
+int timestamp = 1234567890;
+long epochMillis = (long) timestamp; // Noncompliant
+Date date = new Date(epochMillis);
+
+

Compliant solution

+
+long timestamp = 1234567890L;
+long epochMillis = timestamp;
+Date date = new Date(epochMillis);
+
+

Resources

+

Documentation

+ diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9346.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9346.json new file mode 100644 index 00000000000..e6b6c3057d0 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9346.json @@ -0,0 +1,21 @@ +{ + "title": "Integer values should not be cast to long for use as timestamps", + "type": "BUG", + "code": { + "impacts": { + "RELIABILITY": "HIGH" + }, + "attribute": "COMPLETE" + }, + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [], + "defaultSeverity": "Critical", + "ruleSpecification": "RSPEC-9346", + "sqKey": "S9346", + "scope": "All", + "quickfix": "unknown" +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9346 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9346 new file mode 100644 index 00000000000..e69de29bb2d From 58ad9c2ddba64d0d9864db574fe4d02b1c2539cb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:33:53 +0200 Subject: [PATCH 2/4] Update ruling results for PR #5957 (#5958) Co-authored-by: github-actions[bot] --- its/ruling/src/test/resources/eclipse-jetty/java-S9346.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 its/ruling/src/test/resources/eclipse-jetty/java-S9346.json diff --git a/its/ruling/src/test/resources/eclipse-jetty/java-S9346.json b/its/ruling/src/test/resources/eclipse-jetty/java-S9346.json new file mode 100644 index 00000000000..4ed3bcc2f4f --- /dev/null +++ b/its/ruling/src/test/resources/eclipse-jetty/java-S9346.json @@ -0,0 +1,6 @@ +{ +"org.eclipse.jetty:jetty-project:jetty-util-ajax/src/test/java/org/eclipse/jetty/util/ajax/JSONTest.java": [ +366, +433 +] +} From 2d8e0dcd45af6d1f3421d815e6af10832822ad2d Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Wed, 19 Aug 2026 14:07:07 +0200 Subject: [PATCH 3/4] Fix S9346 test assertion, exclude int literals, and update docs - Fix secondary location marker alignment in test sample (off by one space) - Exclude int literal arguments (e.g., `new Date(0)`) from detection to reduce false positives on intentional small values - Update HTML noncompliant example to show patterns the rule actually detects (direct int arg and explicit cast) instead of the variable indirection pattern which is not detected Co-Authored-By: Claude Opus 4.6 --- .../checks/IntegerToLongTimestampCastCheckSample.java | 6 +++--- .../java/checks/IntegerToLongTimestampCastCheck.java | 3 +++ .../resources/org/sonar/l10n/java/rules/java/S9346.html | 8 ++++---- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/IntegerToLongTimestampCastCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/IntegerToLongTimestampCastCheckSample.java index 44a458fb073..adbddc7e40d 100644 --- a/java-checks-test-sources/default/src/main/java/checks/IntegerToLongTimestampCastCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/IntegerToLongTimestampCastCheckSample.java @@ -13,7 +13,7 @@ class IntegerToLongTimestampCastCheckSample { void noncompliantImplicitWidening() { int intVar = 1000; new Date(intVar); // Noncompliant {{Use a "long" value to represent this timestamp.}} -// ^^^^^^ +// ^^^^^^ Instant.ofEpochSecond(intVar); // Noncompliant Instant.ofEpochMilli(intVar); // Noncompliant new Timestamp(intVar); // Noncompliant @@ -72,8 +72,8 @@ void noncompliantParenthesized() { new Date((intVar)); // Noncompliant } - void noncompliantIntLiteral() { - new Date(0); // Noncompliant + void compliantIntLiteral() { + new Date(0); } void compliantLongVariable() { diff --git a/java-checks/src/main/java/org/sonar/java/checks/IntegerToLongTimestampCastCheck.java b/java-checks/src/main/java/org/sonar/java/checks/IntegerToLongTimestampCastCheck.java index 2c407f10375..9aae6e50b6e 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/IntegerToLongTimestampCastCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/IntegerToLongTimestampCastCheck.java @@ -82,6 +82,9 @@ private void checkArgument(ExpressionTree argument) { if (arg.is(Tree.Kind.TYPE_CAST)) { arg = ((TypeCastTree) arg).expression(); } + if (arg.is(Tree.Kind.INT_LITERAL)) { + return; + } Type type = arg.symbolType(); if (type.isUnknown()) { return; diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9346.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9346.html index 69a580697dd..e052308af3d 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9346.html +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9346.html @@ -13,14 +13,14 @@

Why is this an issue?

Noncompliant code example

 int timestamp = 1234567890;
-long epochMillis = (long) timestamp; // Noncompliant
-Date date = new Date(epochMillis);
+Date date = new Date(timestamp);          // Noncompliant — int implicitly widened
+Date date2 = new Date((long) timestamp);  // Noncompliant — cast doesn't fix overflow
 

Compliant solution

 long timestamp = 1234567890L;
-long epochMillis = timestamp;
-Date date = new Date(epochMillis);
+Date date = new Date(timestamp);
+Date date2 = new Date(timestamp);
 

Resources

Documentation

From 0f0799c77481faeba723bedb29320c615a1f54db Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Wed, 19 Aug 2026 15:24:27 +0200 Subject: [PATCH 4/4] Update ruling results for S9346 after excluding int literals The previous commit excluded int literals from S9346, which means the eclipse-jetty findings at JSONTest.java lines 366 and 433 are no longer raised. Co-Authored-By: Claude Opus 4.6 --- .../src/test/resources/eclipse-jetty/java-S9346.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/its/ruling/src/test/resources/eclipse-jetty/java-S9346.json b/its/ruling/src/test/resources/eclipse-jetty/java-S9346.json index 4ed3bcc2f4f..0967ef424bc 100644 --- a/its/ruling/src/test/resources/eclipse-jetty/java-S9346.json +++ b/its/ruling/src/test/resources/eclipse-jetty/java-S9346.json @@ -1,6 +1 @@ -{ -"org.eclipse.jetty:jetty-project:jetty-util-ajax/src/test/java/org/eclipse/jetty/util/ajax/JSONTest.java": [ -366, -433 -] -} +{}