Skip to content

SONARJAVA-7004: Implement S3949: Calculations should not overflow - #6197

Open
nathsou wants to merge 6 commits into
masterfrom
new-rule/S3949
Open

nathsou wants to merge 6 commits into
masterfrom
new-rule/S3949

Conversation

@nathsou

@nathsou nathsou commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • detect high-confidence int and long overflow with exact and bounded range evaluation
  • detect unsafe midpoint formulas while avoiding known-safe constants
  • suppress overlap with S2184, S9354, and S2676
  • add semantic and without-semantic tests with precise issue highlighting

Links

Validation

  • mvn -pl java-checks -am test -Dtest=S3949CheckTest -Dsurefire.failIfNoSpecifiedTests=false
  • generated checklist, metadata, rules-definition, and Sonar way profile tests
  • overlap scan: no high-priority overlap remains

AI disclosure

LLM model used for implementation: GPT-5.6 Sol

@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

SONARJAVA-7004

Comment thread java-checks/src/main/java/org/sonar/java/checks/S3949Check.java
Comment thread java-checks/src/main/java/org/sonar/java/checks/S3949Check.java
Comment thread java-checks/src/main/java/org/sonar/java/checks/S3949Check.java Outdated
Comment thread java-checks/src/main/java/org/sonar/java/checks/S3949Check.java Outdated
@datadog-sonarsource

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

Ruling needs updating. A fix PR has been created: #6198

Please review and merge it into your branch.

Comment thread java-checks/src/main/java/org/sonar/java/checks/S3949Check.java
🤖 Generated with GitHub Actions
@github-actions

Copy link
Copy Markdown
Contributor

Ruling needs updating. A fix PR has been created: #6198

Please review and merge it into your branch.

@nathsou nathsou self-assigned this Sep 18, 2026
@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Ruling Diff Summary

Detected changes in 5 rule files: 0 issues removed, 182 issues added.

S3949 (java) on commons-beanutils - 0 issues removed, 4 issues added - new ruling file

Added src/main/java/org/apache/commons/beanutils2/LazyDynaClass.java (line 245)

       240 |            return;
       241 |         }
       242 | 
       243 |         // Create a new property array with the specified property
       244 |         final DynaProperty[] oldProperties = getDynaProperties();
>>>    245 |         final DynaProperty[] newProperties = new DynaProperty[oldProperties.length+1];
       246 |         System.arraycopy(oldProperties, 0, newProperties, 0, oldProperties.length);
       247 |         newProperties[oldProperties.length] = property;
       248 | 
       249 |        // Update the properties
       250 |        setProperties(newProperties);

Added src/main/java/org/apache/commons/beanutils2/LazyDynaList.java (line 308)

       303 | 
       304 |         if (collection == null || collection.size() == 0) {
       305 |             return false;
       306 |         }
       307 | 
>>>    308 |         ensureCapacity(size() + collection.size());
       309 | 
       310 |         for (final Object e : collection) {
       311 |             add(e);
       312 |         }
       313 | 

Added src/main/java/org/apache/commons/beanutils2/WrapDynaClass.java (line 341)

       336 |         if (mappeds == null) {
       337 |             mappeds = new HashMap<>();
       338 |         }
       339 | 
       340 |         // Construct corresponding DynaProperty information
>>>    341 |         properties = new DynaProperty[regulars.length + mappeds.size()];
       342 |         for (int i = 0; i < regulars.length; i++) {
       343 |             descriptorsMap.put(regulars[i].getName(),
       344 |                     regulars[i]);
       345 |             properties[i] =
       346 |                     new DynaProperty(regulars[i].getName(),

Added src/test/java/org/apache/commons/beanutils2/LazyDynaListTestCase.java (line 288)

       283 |             collection.add(testDynaClass.newInstance());
       284 |             collection.add(testDynaClass.newInstance());
       285 |         } catch(final Exception ex) {
       286 |             fail("1. FAILED: " + ex);
       287 |         }
>>>    288 |         final int expectedSize = dynaArray.length + collection.size();
       289 |         final String origValue = (String)((DynaBean)collection.get(0)).get(testProperty);
       290 |         ((DynaBean)collection.get(0)).set(testProperty, origValue+"_updated_"+0);
       291 |         ((DynaBean)collection.get(1)).set(testProperty, origValue+"_updated_"+1);
       292 |         ((DynaBean)collection.get(2)).set(testProperty, origValue+"_updated_"+2);
       293 | 
S3949 (java) on eclipse-jetty - 0 issues removed, 68 issues added - new ruling file

Added jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java (line 43)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java (line 52)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java (line 54)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java (line 645)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java (line 1348)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java (line 848)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java (line 851)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java (line 852)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java (line 262)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java (line 82)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java (line 123)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java)

Added jetty-http/src/test/java/org/eclipse/jetty/http/GZIPContentDecoderTest.java (line 251)

(source file not found at this revision: jetty-http/src/test/java/org/eclipse/jetty/http/GZIPContentDecoderTest.java)

Added jetty-http/src/test/java/org/eclipse/jetty/http/GZIPContentDecoderTest.java (line 340)

(source file not found at this revision: jetty-http/src/test/java/org/eclipse/jetty/http/GZIPContentDecoderTest.java)

Added jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java (line 249)

(source file not found at this revision: jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java)

Added jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java (line 1043)

(source file not found at this revision: jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java)
S3949 (java) on eclipse-jetty-similar-to-main - 0 issues removed, 35 issues added - new ruling file

Added jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java (line 43)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java (line 52)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java (line 54)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/Http1FieldPreEncoder.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java (line 645)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java (line 1348)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java (line 848)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java (line 851)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java (line 852)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java (line 262)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java (line 82)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java)

Added jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java (line 123)

(source file not found at this revision: jetty-http/src/main/java/org/eclipse/jetty/http/pathmap/ServletPathSpec.java)

Added jetty-http/src/test/java/org/eclipse/jetty/http/GZIPContentDecoderTest.java (line 251)

(source file not found at this revision: jetty-http/src/test/java/org/eclipse/jetty/http/GZIPContentDecoderTest.java)

Added jetty-http/src/test/java/org/eclipse/jetty/http/GZIPContentDecoderTest.java (line 340)

(source file not found at this revision: jetty-http/src/test/java/org/eclipse/jetty/http/GZIPContentDecoderTest.java)

Added jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java (line 249)

(source file not found at this revision: jetty-jmx/src/main/java/org/eclipse/jetty/jmx/MBeanContainer.java)

Added jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java (line 1043)

(source file not found at this revision: jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java)
S3949 (java) on guava - 0 issues removed, 63 issues added - new ruling file

Added src/com/google/common/base/CaseFormat.java (line 152)

       147 |     int i = 0;
       148 |     int j = -1;
       149 |     while ((j = wordBoundary.indexIn(s, ++j)) != -1) {
       150 |       if (i == 0) {
       151 |         // include some extra space for separators
>>>    152 |         out = new StringBuilder(s.length() + 4 * wordSeparator.length());
       153 |         out.append(format.normalizeFirstWord(s.substring(i, j)));
       154 |       } else {
       155 |         out.append(format.normalizeWord(s.substring(i, j)));
       156 |       }
       157 |       out.append(format.wordSeparator);

Added src/com/google/common/base/CharMatcher.java (line 746)

       741 |     if (pos == -1) {
       742 |       return string;
       743 |     }
       744 | 
       745 |     int len = string.length();
>>>    746 |     StringBuilder buf = new StringBuilder((len * 3 / 2) + 16);
       747 | 
       748 |     int oldpos = 0;
       749 |     do {
       750 |       buf.append(string, oldpos, pos);
       751 |       buf.append(replacement);

Added src/com/google/common/base/CharMatcher.java (line 1081)

      1076 |       return new String(array);
      1077 |     }
      1078 | 
      1079 |     @Override
      1080 |     public String replaceFrom(CharSequence sequence, CharSequence replacement) {
>>>   1081 |       StringBuilder result = new StringBuilder(sequence.length() * replacement.length());
      1082 |       for (int i = 0; i < sequence.length(); i++) {
      1083 |         result.append(replacement);
      1084 |       }
      1085 |       return result.toString();
      1086 |     }

Added src/com/google/common/base/Joiner.java (line 457)

       452 |       final Object first, final Object second, final Object[] rest) {
       453 |     checkNotNull(rest);
       454 |     return new AbstractList<Object>() {
       455 |       @Override
       456 |       public int size() {
>>>    457 |         return rest.length + 2;
       458 |       }
       459 | 
       460 |       @Override
       461 |       public Object get(int index) {
       462 |         switch (index) {

Added src/com/google/common/base/Preconditions.java (line 413)

       408 |   // Note that this is somewhat-improperly used from Verify.java as well.
       409 |   static String format(String template, @Nullable Object... args) {
       410 |     template = String.valueOf(template); // null -> "null"
       411 | 
       412 |     // start substituting the arguments into the '%s' placeholders
>>>    413 |     StringBuilder builder = new StringBuilder(template.length() + 16 * args.length);
       414 |     int templateStart = 0;
       415 |     int i = 0;
       416 |     while (i < args.length) {
       417 |       int placeholderStart = template.indexOf("%s", templateStart);
       418 |       if (placeholderStart == -1) {

Added src/com/google/common/collect/ArrayTable.java (line 519)

       514 | 
       515 |   // TODO(jlevy): Add eraseRow and eraseColumn methods?
       516 | 
       517 |   @Override
       518 |   public int size() {
>>>    519 |     return rowList.size() * columnList.size();
       520 |   }
       521 | 
       522 |   /**
       523 |    * Returns an unmodifiable set of all row key / column key / value
       524 |    * triplets. Changes to the table will update the returned set.

Added src/com/google/common/collect/CartesianList.java (line 54)

        49 |     return new CartesianList<E>(axesBuilder.build());
        50 |   }
        51 | 
        52 |   CartesianList(ImmutableList<List<E>> axes) {
        53 |     this.axes = axes;
>>>     54 |     int[] axesSizeProduct = new int[axes.size() + 1];
        55 |     axesSizeProduct[axes.size()] = 1;
        56 |     try {
        57 |       for (int i = axes.size() - 1; i >= 0; i--) {
        58 |         axesSizeProduct[i] = IntMath.checkedMultiply(axesSizeProduct[i + 1], axes.get(i).size());
        59 |       }

Added src/com/google/common/collect/EnumHashBiMap.java (line 122)

       117 |   private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
       118 |     stream.defaultReadObject();
       119 |     keyType = (Class<K>) stream.readObject();
       120 |     setDelegates(
       121 |         WellBehavedMap.wrap(new EnumMap<K, V>(keyType)),
>>>    122 |         new HashMap<V, K>(keyType.getEnumConstants().length * 3 / 2));
       123 |     Serialization.populateMap(this, stream);
       124 |   }
       125 | 
       126 |   @GwtIncompatible("only needed in emulated source.")
       127 |   private static final long serialVersionUID = 0;

Added src/com/google/common/collect/HashBiMap.java (line 342)

       337 |   }
       338 | 
       339 |   private void rehashIfNecessary() {
       340 |     BiEntry<K, V>[] oldKToV = hashTableKToV;
       341 |     if (Hashing.needsResizing(size, oldKToV.length, LOAD_FACTOR)) {
>>>    342 |       int newTableSize = oldKToV.length * 2;
       343 | 
       344 |       this.hashTableKToV = createTable(newTableSize);
       345 |       this.hashTableVToK = createTable(newTableSize);
       346 |       this.mask = newTableSize - 1;
       347 |       this.size = 0;

Added src/com/google/common/collect/ImmutableList.java (line 182)

       177 |    * @throws NullPointerException if any element is null
       178 |    * @since 3.0 (source-compatible since 2.0)
       179 |    */
       180 |   public static <E> ImmutableList<E> of(
       181 |       E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10, E e11, E e12, E... others) {
>>>    182 |     Object[] array = new Object[12 + others.length];
       183 |     array[0] = e1;
       184 |     array[1] = e2;
       185 |     array[2] = e3;
       186 |     array[3] = e4;
       187 |     array[4] = e5;

Added src/com/google/common/collect/ImmutableSet.java (line 108)

       103 |    *
       104 |    * @since 3.0 (source-compatible since 2.0)
       105 |    */
       106 |   public static <E> ImmutableSet<E> of(E e1, E e2, E e3, E e4, E e5, E e6, E... others) {
       107 |     final int paramCount = 6;
>>>    108 |     Object[] elements = new Object[paramCount + others.length];
       109 |     elements[0] = e1;
       110 |     elements[1] = e2;
       111 |     elements[2] = e3;
       112 |     elements[3] = e4;
       113 |     elements[4] = e5;

Added src/com/google/common/collect/ImmutableSortedMultiset.java (line 132)

       127 |    * @throws NullPointerException if any element is null
       128 |    */
       129 |   @SuppressWarnings("unchecked")
       130 |   public static <E extends Comparable<? super E>> ImmutableSortedMultiset<E> of(
       131 |       E e1, E e2, E e3, E e4, E e5, E e6, E... remaining) {
>>>    132 |     int size = remaining.length + 6;
       133 |     List<E> all = Lists.newArrayListWithCapacity(size);
       134 |     Collections.addAll(all, e1, e2, e3, e4, e5, e6);
       135 |     Collections.addAll(all, remaining);
       136 |     return copyOf(Ordering.natural(), all);
       137 |   }

Added src/com/google/common/collect/ImmutableSortedMultiset.java (line 263)

       258 |       Comparator<? super E> comparator, Collection<Entry<E>> entries) {
       259 |     if (entries.isEmpty()) {
       260 |       return emptyMultiset(comparator);
       261 |     }
       262 |     ImmutableList.Builder<E> elementsBuilder = new ImmutableList.Builder<E>(entries.size());
>>>    263 |     long[] cumulativeCounts = new long[entries.size() + 1];
       264 |     int i = 0;
       265 |     for (Entry<E> entry : entries) {
       266 |       elementsBuilder.add(entry.getElement());
       267 |       cumulativeCounts[i + 1] = cumulativeCounts[i] + entry.getCount();
       268 |       i++;

Added src/com/google/common/collect/ImmutableSortedSet.java (line 149)

       144 |    * @since 3.0 (source-compatible since 2.0)
       145 |    */
       146 |   @SuppressWarnings("unchecked")
       147 |   public static <E extends Comparable<? super E>> ImmutableSortedSet<E> of(
       148 |       E e1, E e2, E e3, E e4, E e5, E e6, E... remaining) {
>>>    149 |     Comparable[] contents = new Comparable[6 + remaining.length];
       150 |     contents[0] = e1;
       151 |     contents[1] = e2;
       152 |     contents[2] = e3;
       153 |     contents[3] = e4;
       154 |     contents[4] = e5;

Added src/com/google/common/collect/LinkedHashMultimap.java (line 443)

       438 |     }
       439 | 
       440 |     private void rehashIfNecessary() {
       441 |       if (Hashing.needsResizing(size, hashTable.length, VALUE_SET_LOAD_FACTOR)) {
       442 |         @SuppressWarnings("unchecked")
>>>    443 |         ValueEntry<K, V>[] hashTable = new ValueEntry[this.hashTable.length * 2];
       444 |         this.hashTable = hashTable;
       445 |         int mask = hashTable.length - 1;
       446 |         for (ValueSetLink<K, V> entry = firstEntry;
       447 |             entry != this; entry = entry.getSuccessorInValueSet()) {
       448 |           ValueEntry<K, V> valueEntry = (ValueEntry<K, V>) entry;
S3949 (java) on sonar-server - 0 issues removed, 12 issues added - new ruling file

Added src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java (line 185)

(source file not found at this revision: src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java)

Added src/main/java/org/sonar/server/computation/task/projectanalysis/filemove/FileMoveDetectionStep.java (line 325)

(source file not found at this revision: src/main/java/org/sonar/server/computation/task/projectanalysis/filemove/FileMoveDetectionStep.java)

Added src/main/java/org/sonar/server/computation/task/projectanalysis/filemove/SourceSimilarityImpl.java (line 39)

(source file not found at this revision: src/main/java/org/sonar/server/computation/task/projectanalysis/filemove/SourceSimilarityImpl.java)

Added src/main/java/org/sonar/server/computation/task/projectanalysis/filemove/SourceSimilarityImpl.java (line 40)

(source file not found at this revision: src/main/java/org/sonar/server/computation/task/projectanalysis/filemove/SourceSimilarityImpl.java)

Added src/main/java/org/sonar/server/duplication/ws/ShowResponseBuilder.java (line 79)

(source file not found at this revision: src/main/java/org/sonar/server/duplication/ws/ShowResponseBuilder.java)

Added src/main/java/org/sonar/server/issue/IssueStorage.java (line 102)

(source file not found at this revision: src/main/java/org/sonar/server/issue/IssueStorage.java)

Added src/main/java/org/sonar/server/issue/IssueStorage.java (line 105)

(source file not found at this revision: src/main/java/org/sonar/server/issue/IssueStorage.java)

Added src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java (line 135)

(source file not found at this revision: src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java)

Added src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java (line 149)

(source file not found at this revision: src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java)

Added src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java (line 192)

(source file not found at this revision: src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java)

Added src/main/java/org/sonar/server/qualityprofile/ws/DeleteAction.java (line 112)

(source file not found at this revision: src/main/java/org/sonar/server/qualityprofile/ws/DeleteAction.java)

Added src/main/java/org/sonar/server/setting/ws/SetAction.java (line 203)

(source file not found at this revision: src/main/java/org/sonar/server/setting/ws/SetAction.java)

@github-actions

Copy link
Copy Markdown
Contributor

Ruling needs updating. A fix PR has been created: #6200

Please review and merge it into your branch.

Comment thread java-checks-test-sources/default/src/main/java/checks/S3949CheckSample.java Outdated
Comment on lines +121 to +135
private static boolean isUnsafeMidpoint(BinaryExpressionTree addition) {
if (!addition.symbolType().isPrimitive(Type.Primitives.INT)) {
return false;
}
Tree parent = ExpressionUtils.skipParenthesesUpwards(addition.parent());
if (!(parent instanceof BinaryExpressionTree division) || !division.is(Tree.Kind.DIVIDE)
|| ExpressionUtils.skipParentheses(division.leftOperand()) != addition) {
return false;
}
Integer denominator = ExpressionUtils.skipParentheses(division.rightOperand()).asConstant(Integer.class).orElse(null);
Long longDenominator = ExpressionUtils.skipParentheses(division.rightOperand()).asConstant(Long.class).orElse(null);
Double floatingDenominator = LiteralUtils.doubleLiteralValue(ExpressionUtils.skipParentheses(division.rightOperand()));
if (!Integer.valueOf(2).equals(denominator) && !Long.valueOf(2L).equals(longDenominator)
&& !Double.valueOf(2.0).equals(floatingDenominator)) {
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Bug: Midpoint heuristic still flags non-overflowing narrow operands

The new guard only requires the addition's own type to be int, but byte/short/char operands are promoted to int, so (low + high) / 2 with two short (or byte/char) parameters still reaches the heuristic; IntegerOverflowRange.rangeOf returns null for such unknown identifiers and isUnsafeMidpoint treats left == null || right == null as unsafe, raising an issue on a sum that mathematically cannot exceed int range. Restrict the heuristic to additions whose operands are themselves int/long typed so promoted narrow operands are not reported.

Require both operands to be int before applying the midpoint heuristic:

private static boolean isUnsafeMidpoint(BinaryExpressionTree addition) {
  if (!addition.symbolType().isPrimitive(Type.Primitives.INT)
    || !addition.leftOperand().symbolType().isPrimitive(Type.Primitives.INT)
    || !addition.rightOperand().symbolType().isPrimitive(Type.Primitives.INT)) {
    return false;
  }
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

gitar-bot[bot]

This comment was marked as resolved.

@github-actions

Copy link
Copy Markdown
Contributor

Ruling needs updating. A fix PR has been created: #6200

Please review and merge it into your branch.

github-actions Bot and others added 2 commits September 18, 2026 15:51
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
@gitar-bot
gitar-bot Bot dismissed their stale review September 18, 2026 13:52

✅ All blocking issues resolved.

Configure merge blocking

@gitar-bot

gitar-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 8 closed / 9 findings

🟡 Medium risk

Implements S3949 to detect integer and long overflow with exact and bounded range evaluation, including unsafe midpoint formulas while suppressing overlap with related rules. The implementation is comprehensive with semantic and non-semantic tests, but the midpoint heuristic still flags non-overflowing narrow operands (byte/short/char) that are promoted to int—restrict the heuristic to additions whose operands are themselves int/long typed to avoid false positives on mathematically safe sums.

💡 Bug: Midpoint heuristic still flags non-overflowing narrow operands

📄 java-checks/src/main/java/org/sonar/java/checks/S3949Check.java:121-135

The new guard only requires the addition's own type to be int, but byte/short/char operands are promoted to int, so (low + high) / 2 with two short (or byte/char) parameters still reaches the heuristic; IntegerOverflowRange.rangeOf returns null for such unknown identifiers and isUnsafeMidpoint treats left == null || right == null as unsafe, raising an issue on a sum that mathematically cannot exceed int range. Restrict the heuristic to additions whose operands are themselves int/long typed so promoted narrow operands are not reported.

Require both operands to be int before applying the midpoint heuristic
private static boolean isUnsafeMidpoint(BinaryExpressionTree addition) {
  if (!addition.symbolType().isPrimitive(Type.Primitives.INT)
    || !addition.leftOperand().symbolType().isPrimitive(Type.Primitives.INT)
    || !addition.rightOperand().symbolType().isPrimitive(Type.Primitives.INT)) {
    return false;
  }
✅ 8 closed
Bug: Bounded ranges cause FPs on length()+1 and size()*k

📄 java-checks/src/main/java/org/sonar/java/checks/S3949Check.java:83-97 📄 java-checks/src/main/java/org/sonar/java/checks/helpers/IntegerOverflowRange.java:36 📄 java-checks/src/main/java/org/sonar/java/checks/helpers/IntegerOverflowRange.java:99-107
checkBinary reports as soon as one operand has an exact range and the mathematical result leaves the type range, while the other operand may carry the very wide bounded range [0, Integer.MAX_VALUE] produced by NON_NEGATIVE_INT for length()/size()/ordinal()/array length. Consequently ubiquitous code such as int n = text.length() + 1; (result [1, 2147483648]) or int n = list.size() * 2; is reported, even though the overflow needs a collection/string of ~2^31 elements. Bounded ranges are useful to prove safety (as the hashCode() & 255 case shows) but should not by themselves justify an issue.

Quality: Compound-assignment handling is unreachable and untested

📄 java-checks/src/main/java/org/sonar/java/checks/S3949Check.java:55-58 📄 java-checks/src/main/java/org/sonar/java/checks/S3949Check.java:112-126 📄 java-checks/src/main/java/org/sonar/java/checks/helpers/IntegerOverflowRange.java:124-131
checkCompound can never report: a variable that is the target of +=/-=/*= is neither final nor effectively final, so ExpressionsHelper.isNotReassigned fails and IntegerOverflowRange.rangeOf(tree.variable()) returns null for identifiers (and returns null for member selects and array accesses as well), making the method return before the switch. The three *_ASSIGNMENT kinds therefore only add traversal work, and the sample file contains no compound-assignment case, so lines 118-126 have no coverage at all.

Bug: Return suppression uses enclosing method through a lambda

📄 java-checks/src/main/java/org/sonar/java/checks/S3949Check.java:179-193
enclosingMethod walks up to the first MethodTree, skipping any LambdaExpressionTree in between. For a lambda with a block body, e.g. long f() { Supplier<Integer> s = () -> { return Integer.MAX_VALUE + 1; }; ... }, the return statement is matched against the enclosing method's long return type, so isWidenedSink reports the addition as widened and the genuine int overflow is silently dropped.

Bug: Midpoint heuristic flags the documented widening fix

📄 java-checks/src/main/java/org/sonar/java/checks/S3949Check.java:66-68 📄 java-checks/src/main/java/org/sonar/java/checks/S3949Check.java:129-143 📄 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3949.html:6-7 📄 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3949.html:43-48
isUnsafeMidpoint returns true whenever either operand range is unknown, without looking at the type in which the addition is performed. So int mid = (int) (((long) low + high) / 2); — the widening fix the rule description itself recommends ("At least one operand must be widened before the operation") — is still reported, and every (a + b) / 2 over unknown long values is reported too, although those need values near 2^63. The check only whitelists low + (high - low) / 2 and >>> 1.

Quality: Rule description shows examples the check never raises

📄 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3949.html:34-41 📄 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S3949.html:50-57 📄 java-checks/src/main/java/org/sonar/java/checks/S3949Check.java:102-110 📄 java-checks/src/main/java/org/sonar/java/checks/S3949Check.java:145-148 📄 java-checks-test-sources/default/src/main/java/checks/S3949CheckSample.java:71-72
Two noncompliant examples in the shipped description cannot be raised by this implementation: -Integer.MIN_VALUE (example 4) is explicitly suppressed by isMinimumValue, which the sample file confirms by listing int minimumNegation = -Integer.MIN_VALUE; as compliant, and Integer.MAX_VALUE + increment (example 6) returns early because rangeOf yields null for a non-resolvable operand. Users following the description will expect issues that never appear. Since negation can only overflow for exactly MIN_VALUE, the suppression also leaves UNARY_MINUS handling without any real trigger.

...and 3 more closed from earlier reviews

🤖 Prompt for agents
Code Review: Implements S3949 to detect integer and long overflow with exact and bounded range evaluation, including unsafe midpoint formulas while suppressing overlap with related rules. The implementation is comprehensive with semantic and non-semantic tests, but the midpoint heuristic still flags non-overflowing narrow operands (`byte`/`short`/`char`) that are promoted to `int`—restrict the heuristic to additions whose operands are themselves `int`/`long` typed to avoid false positives on mathematically safe sums.

1. 💡 Bug: Midpoint heuristic still flags non-overflowing narrow operands
   Files: java-checks/src/main/java/org/sonar/java/checks/S3949Check.java:121-135

   The new guard only requires the addition's own type to be `int`, but `byte`/`short`/`char` operands are promoted to `int`, so `(low + high) / 2` with two `short` (or `byte`/`char`) parameters still reaches the heuristic; `IntegerOverflowRange.rangeOf` returns `null` for such unknown identifiers and `isUnsafeMidpoint` treats `left == null || right == null` as unsafe, raising an issue on a sum that mathematically cannot exceed `int` range. Restrict the heuristic to additions whose operands are themselves `int`/`long` typed so promoted narrow operands are not reported.

   Fix (Require both operands to be int before applying the midpoint heuristic):
   private static boolean isUnsafeMidpoint(BinaryExpressionTree addition) {
     if (!addition.symbolType().isPrimitive(Type.Primitives.INT)
       || !addition.leftOperand().symbolType().isPrimitive(Type.Primitives.INT)
       || !addition.rightOperand().symbolType().isPrimitive(Type.Primitives.INT)) {
       return false;
     }

Review coverage

Functional validation 4 of 4 objectives covered

Rules No rules evaluated

Auto-approval Not enabled · Set up

Implementation Status ✅ 4 of 4 objectives covered
SONARJAVA-7004 - 4 of 4 objectives covered

This PR implements rule S3949 to detect integer calculation overflow, utilizing expression-tree visitors and semantic constant evaluation for addition, subtraction, multiplication, negation, and midpoints, while adding appropriate unit tests.

✅ 4 covered here
  • ✅ Implement rule S3949 to detect calculations that may overflow
  • ✅ Use expression-tree visitors and semantic constant evaluation for integer addition, subtraction, multiplication, unary negation, and midpoint patterns
  • ✅ Report only when overflow is provable or structurally clear while suppressing safe or already covered cases
  • ✅ Add semantic and without-semantic tests for S3949
Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Counting what did not apply, without listing it.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqube-next

Copy link
Copy Markdown
Contributor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant