RIS-492 Create rule S8265: Zone IDs passed to "ZoneId.of()" should be valid#5677
RIS-492 Create rule S8265: Zone IDs passed to "ZoneId.of()" should be valid#5677romainbrenguier wants to merge 5 commits into
Conversation
Generated rule stubs and RSPEC specification for detecting invalid zone ID strings passed to ZoneId.of() method, which would throw ZoneRulesException at runtime. Generated files: - ValidZoneIdCheck.java: Check implementation stub - ValidZoneIdCheckTest.java: Test stub - ValidZoneIdCheckSample.java: Sample test cases - S8265.html/json: RSPEC specification from PR 5849 Rule metadata: - Type: BUG - Severity: Critical - Impact: RELIABILITY HIGH - Jira: RIS-492 - RSPEC PR: SonarSource/rspec#5849 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Created plan_rule.py script to generate planning artifacts using Claude API - Generated technical_spec_S8265.md with implementation approach and patterns - Generated comprehensive test cases in ValidZoneIdCheckSample.java - Test cases cover noncompliant (invalid zones, abbreviations, typos) and compliant (IANA zones, offsets, special IDs) scenarios Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Created implement_rule.py script to generate Java implementation using Claude API - Implemented ValidZoneIdCheck extending AbstractMethodDetection - Validates string literals passed to ZoneId.of() against IANA database - Includes Levenshtein distance algorithm for typo suggestions - Maps common abbreviations (PST, EST, etc.) to correct zone IDs - Updated test cases to match actual validation behavior - All tests pass successfully Implementation features: - Detects invalid zone IDs, three-letter abbreviations, typos - Provides helpful suggestions for common mistakes - Skips dynamic strings to avoid false positives - Validates against IANA zones, fixed offsets, special IDs Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Added markdown code fence cleanup in plan_rule.py - Ensured newline at end of generated files - Updated technical specification with clearer descriptions Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Agentic Analysis: Early ResultsAgentic Analysis and Context Augmentation are available on your project. Here are some issues that could have been prevented. Follow the links to learn how to put them into action. 16 issue(s) found across 4 file(s):
Analyzed by SonarQube Agentic Analysis in 7.1 s |
- Created create_pr.py adapted from sonar-skunk's create-pr task - Uses gh CLI to create draft pull requests - Generates detailed PR description with implementation details - Handles existing PR detection and reuse - Successfully created PR #5677 for S8265 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
| private static final Pattern FIXED_OFFSET_PATTERN = Pattern.compile( | ||
| "^[+-]\\d{1,2}(:\\d{2}(:\\d{2})?)?$|^[+-]\\d{4}$" | ||
| ); | ||
|
|
||
| private static final Pattern ZONE_OFFSET_PATTERN = Pattern.compile( | ||
| "^(UTC|GMT)[+-]\\d{1,2}(:\\d{2})?$" | ||
| ); |
There was a problem hiding this comment.
⚠️ Bug: Valid "UT"-prefixed offsets flagged as invalid (false positive)
ZoneId.of() accepts three offset prefixes: UTC, GMT, and UT (e.g. ZoneId.of("UT+1") is valid). ZONE_OFFSET_PATTERN only matches ^(UTC|GMT)[+-]..., so UT+1/UT-5 are reported as invalid even though they are accepted at runtime. Because this rule is typed BUG/Critical, a false positive here is harmful. The pattern also rejects valid forms that ZoneId.of accepts, e.g. prefixed offsets with seconds (UTC+05:30:00) and bare-digit offsets such as +053000/+053000 style 6-digit offsets, since FIXED_OFFSET_PATTERN only allows the [+-]\d{4} form. Consider delegating validation to the JDK (e.g. try ZoneId.of / ZoneOffset.of in a guarded manner) instead of hand-rolled regexes to keep behavior aligned with the runtime.
Was this helpful? React with 👍 / 👎
| private String suggestAlternative(String invalidZoneId) { | ||
| if (COMMON_CORRECTIONS.containsKey(invalidZoneId)) { | ||
| return COMMON_CORRECTIONS.get(invalidZoneId); | ||
| } | ||
|
|
||
| return VALID_ZONE_IDS.stream() | ||
| .min(Comparator.comparingInt(valid -> levenshteinDistance(invalidZoneId, valid))) | ||
| .filter(closest -> levenshteinDistance(invalidZoneId, closest) <= 3) | ||
| .orElse(null); | ||
| } |
There was a problem hiding this comment.
⚠️ Quality: Suggested zone ID is non-deterministic (HashSet iteration order)
suggestAlternative picks the closest valid zone via VALID_ZONE_IDS.stream().min(comparingInt(levenshtein)). ZoneId.getAvailableZoneIds() returns an unordered Set (HashSet), so when multiple zone IDs tie at the minimum Levenshtein distance, min() returns whichever appears first in iteration order. That order is not guaranteed across JDK versions / tz-database updates. The expected messages in the test sample hardcode specific suggestions (e.g. ZoneId.of("") → Did you mean "GB"?, and the typo cases), which makes the test brittle and likely to break on a different JDK/tz version. For an empty string, many 2-character zone IDs tie, so the chosen suggestion is effectively arbitrary. Sort deterministically (e.g. tie-break by natural string order) and/or suppress suggestions when the distance is large/ambiguous to keep messages stable.
Deterministic tie-breaking and single filter; avoids HashSet-order dependence.:
return VALID_ZONE_IDS.stream()
.filter(valid -> levenshteinDistance(invalidZoneId, valid) <= 3)
.min(Comparator
.comparingInt((String valid) -> levenshteinDistance(invalidZoneId, valid))
.thenComparing(Comparator.naturalOrder()))
.orElse(null);
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
| return VALID_ZONE_IDS.stream() | ||
| .min(Comparator.comparingInt(valid -> levenshteinDistance(invalidZoneId, valid))) | ||
| .filter(closest -> levenshteinDistance(invalidZoneId, closest) <= 3) | ||
| .orElse(null); |
There was a problem hiding this comment.
💡 Performance: suggestAlternative recomputes Levenshtein over all zone IDs repeatedly
For every invalid string literal, suggestAlternative streams over all ~600 entries of VALID_ZONE_IDS. The min(Comparator.comparingInt(... levenshteinDistance ...)) recomputes the (O(len^2)) Levenshtein distance on every pairwise comparison, and then .filter(closest -> levenshteinDistance(...) <= 3) computes it again for the winner. This is wasteful: the comparator evaluates the key function multiple times per element. Prefer materializing the distance once per candidate (e.g. map to a pair, or filter-then-min as in the suggested fix) so each zone ID's distance is computed at most once.
Was this helpful? React with 👍 / 👎
| #!/usr/bin/env python3 | ||
| """ | ||
| Plan rule implementation for S8265 ValidZoneIdCheck. | ||
| Generates technical specification and test cases based on RSPEC. | ||
| """ | ||
|
|
||
| import json | ||
| import os | ||
| import sys | ||
| from pathlib import Path | ||
| from anthropic import Anthropic | ||
|
|
||
| def load_rspec_files(): | ||
| """Load RSPEC HTML and metadata files.""" | ||
| html_path = Path("sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8265.html") |
There was a problem hiding this comment.
💡 Quality: AI scaffolding scripts and generated spec committed to the repo
plan_rule.py, implement_rule.py, and technical_spec_S8265.md are code-generation helper scripts/notes (they call the Anthropic API to author the check) committed at the repository root. These are developer scaffolding rather than product code: they are not wired into the build, reference an incorrect rule key in the spec (S6885/InvalidZoneIdCheck vs the actual S8265/ValidZoneIdCheck), and would normally not belong in the plugin repo. Consider removing them from the PR (or moving them to a tooling location that is clearly out of the shipped artifact) before merge.
Was this helpful? React with 👍 / 👎
|
|
||
| return dp[s1.length()][s2.length()]; | ||
| } | ||
| } No newline at end of file |
There was a problem hiding this comment.
💡 Quality: Missing trailing newline at end of ValidZoneIdCheck.java
ValidZoneIdCheck.java ends without a trailing newline (\ No newline at end of file). Most files in this codebase end with a newline and license/format checks may flag this. Add a final newline.
Was this helpful? React with 👍 / 👎
| if result.returncode != 0: | ||
| # Check if error is due to existing PR | ||
| if "already exists" in result.stderr.lower(): | ||
| print("PR already exists for this branch") | ||
| existing_pr = check_existing_pr(branch_name) | ||
| if existing_pr: | ||
| print(f"Existing PR: {existing_pr['url']}") | ||
| return existing_pr['url'] | ||
| else: | ||
| print(f"Error creating PR: {result.stderr}") | ||
| sys.exit(1) | ||
|
|
||
| pr_url = result.stdout.strip() | ||
| print(f"Created draft PR: {pr_url}") | ||
| return pr_url |
There was a problem hiding this comment.
💡 Bug: create_pr.py prints empty PR URL when 'already exists' but lookup fails
In create_pull_request, when gh pr create fails with returncode != 0 and stderr contains "already exists" but check_existing_pr returns None (e.g., the PR is on a different base, closed, or not returned by the list query), the code does not return or exit. Execution falls through to pr_url = result.stdout.strip(), which is empty on failure, and prints Created draft PR: with no URL — silently reporting success despite no PR being created/found. Add an explicit return/exit in the already exists branch when no existing PR is found.
Exit instead of falling through to an empty URL when the existing PR cannot be found.:
if result.returncode != 0:
if "already exists" in result.stderr.lower():
print("PR already exists for this branch")
existing_pr = check_existing_pr(branch_name)
if existing_pr:
print(f"Existing PR: {existing_pr['url']}")
return existing_pr['url']
print("Could not locate the existing PR")
sys.exit(1)
print(f"Error creating PR: {result.stderr}")
sys.exit(1)
- Apply fix
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎
|
CI failed: The build succeeded, but generated a diff artifact indicating that the autoscan output changed due to the implementation of the new rule S8265.OverviewThe build completed successfully, but the automated regression testing (autoscan) detected a diff in the output. This is a common occurrence when introducing a new static analysis rule, as it expects the new rule to identify additional violations in test code. FailuresAutoscan output mismatch (confidence: high)
Summary
Code Review
|
| Auto-apply | Compact | Unblock |
|
|
|
Was this helpful? React with 👍 / 👎 | Gitar




Rule: S8265 - Zone IDs passed to "ZoneId.of()" should be valid
Type: BUG
Severity: Critical
Tags: datetime, pitfall
Description
This rule detects invalid time zone identifier strings passed to
ZoneId.of()method.Implementation Details
AbstractMethodDetectionto detectZoneId.of(String)callsZoneId.getAvailableZoneIds())Test Coverage
All tests pass:
Related Issues
🤖 Generated with Claude Code (claude.com/code)