-
Notifications
You must be signed in to change notification settings - Fork 725
RIS-492 Create rule S8265: Zone IDs passed to "ZoneId.of()" should be valid #5677
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
romainbrenguier
wants to merge
5
commits into
master
Choose a base branch
from
romain/s8265
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
76a16d2
Create rule S8265: Zone IDs passed to "ZoneId.of()" should be valid
romainbrenguier d2dd39f
Generate technical specification and test cases for S8265
romainbrenguier 0a012a2
Implement S8265 ValidZoneIdCheck for invalid ZoneId detection
romainbrenguier 54306c1
Improve plan_rule.py script and update technical specification
romainbrenguier be8eef6
Add create_pr.py script for automated PR creation
romainbrenguier File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| Create GitHub pull request for S8265 ValidZoneIdCheck. | ||
| Adapted from sonar-skunk's rule-factory create_pr task. | ||
| """ | ||
|
|
||
| import json | ||
| import subprocess | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| def get_rule_metadata(): | ||
| """Load rule metadata.""" | ||
| metadata_path = Path("sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8265.json") | ||
| with open(metadata_path, 'r') as f: | ||
| return json.load(f) | ||
|
|
||
| def get_branch_name(): | ||
| """Get current branch name.""" | ||
| result = subprocess.run( | ||
| ["git", "branch", "--show-current"], | ||
| capture_output=True, | ||
| text=True, | ||
| check=True | ||
| ) | ||
| return result.stdout.strip() | ||
|
|
||
| def push_branch(branch_name): | ||
| """Push branch to origin.""" | ||
| print(f"Pushing branch {branch_name} to origin...") | ||
| subprocess.run( | ||
| ["git", "push", "-u", "origin", branch_name], | ||
| check=True | ||
| ) | ||
| print(f"Pushed branch {branch_name}") | ||
|
|
||
| def check_existing_pr(branch_name): | ||
| """Check if PR already exists for this branch.""" | ||
| result = subprocess.run( | ||
| ["gh", "pr", "list", "--head", branch_name, "--json", "number,url,title"], | ||
| capture_output=True, | ||
| text=True, | ||
| check=True | ||
| ) | ||
| prs = json.loads(result.stdout) | ||
| return prs[0] if prs else None | ||
|
|
||
| def generate_pr_body(metadata): | ||
| """Generate PR body content.""" | ||
| title = metadata['title'] | ||
| rule_type = metadata['type'] | ||
| severity = metadata['defaultSeverity'] | ||
| tags = ', '.join(metadata.get('tags', [])) | ||
|
|
||
| return f"""## Rule: S8265 - {title} | ||
|
|
||
| **Type:** {rule_type} | ||
| **Severity:** {severity} | ||
| **Tags:** {tags} | ||
|
|
||
| ### Description | ||
|
|
||
| This rule detects invalid time zone identifier strings passed to `ZoneId.of()` method. | ||
|
|
||
| ### Implementation Details | ||
|
|
||
| - Extends `AbstractMethodDetection` to detect `ZoneId.of(String)` calls | ||
| - Validates string literals against: | ||
| - IANA Time Zone Database identifiers (using `ZoneId.getAvailableZoneIds()`) | ||
| - Fixed offset formats (+05:30, -08:00, etc.) | ||
| - Special identifiers (UTC, GMT, Z, UT) | ||
| - Zone offset IDs (UTC+1, GMT-5) | ||
| - Provides helpful suggestions for common mistakes using Levenshtein distance | ||
| - Maps three-letter abbreviations (PST, EST, etc.) to correct zone IDs | ||
|
|
||
| ### Test Coverage | ||
|
|
||
| All tests pass: | ||
| - Invalid zone IDs detection | ||
| - Three-letter abbreviations with suggestions | ||
| - Typo detection with suggestions | ||
| - Case sensitivity validation | ||
| - Dynamic string handling (no false positives) | ||
|
|
||
| ### Related Issues | ||
|
|
||
| - Jira: RIS-492 | ||
| - RSPEC PR: https://github.com/SonarSource/rspec/pull/5849 | ||
|
|
||
| --- | ||
|
|
||
| 🤖 Generated with Claude Code (claude.com/code) | ||
| """ | ||
|
|
||
| def create_pull_request(branch_name, metadata, jira_key="RIS-492"): | ||
| """Create draft pull request using gh CLI.""" | ||
| title = metadata['title'] | ||
| pr_title = f"{jira_key} Create rule S8265: {title}" | ||
| pr_body = generate_pr_body(metadata) | ||
|
|
||
| print(f"Creating draft PR for {branch_name}...") | ||
| print(f"Title: {pr_title}") | ||
|
|
||
| result = subprocess.run( | ||
| ["gh", "pr", "create", | ||
| "--draft", | ||
| "--title", pr_title, | ||
| "--body", pr_body, | ||
| "--head", branch_name, | ||
| "--base", "master"], | ||
| capture_output=True, | ||
| text=True | ||
| ) | ||
|
|
||
| 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 | ||
|
|
||
| def main(): | ||
| print("Loading rule metadata...") | ||
| metadata = get_rule_metadata() | ||
|
|
||
| print("Getting current branch...") | ||
| branch_name = get_branch_name() | ||
| print(f"Branch: {branch_name}") | ||
|
|
||
| # Check if PR already exists | ||
| existing_pr = check_existing_pr(branch_name) | ||
| if existing_pr: | ||
| print(f"PR already exists for branch {branch_name}") | ||
| print(f"URL: {existing_pr['url']}") | ||
| print(f"Title: {existing_pr['title']}") | ||
| return | ||
|
|
||
| # Push branch to origin | ||
| push_branch(branch_name) | ||
|
|
||
| # Create PR | ||
| pr_url = create_pull_request(branch_name, metadata) | ||
|
|
||
| print("\nCreate-PR complete!") | ||
| print(f"\nPull Request: {pr_url}") | ||
| print(f"\nNext steps:") | ||
| print(f"1. Review the PR description") | ||
| print(f"2. Ensure all CI checks pass") | ||
| print(f"3. Mark as ready for review when complete") | ||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| Implement rule S8265 ValidZoneIdCheck. | ||
| Generates the Java implementation based on technical specification and test cases. | ||
| """ | ||
|
|
||
| import json | ||
| import os | ||
| import sys | ||
| from pathlib import Path | ||
| from anthropic import Anthropic | ||
|
|
||
| def load_planning_files(): | ||
| """Load technical specification, test cases, and metadata.""" | ||
| tech_spec_path = Path("technical_spec_S8265.md") | ||
| test_cases_path = Path("java-checks-test-sources/default/src/main/java/checks/ValidZoneIdCheckSample.java") | ||
| metadata_path = Path("sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8265.json") | ||
| html_path = Path("sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8265.html") | ||
|
|
||
| with open(tech_spec_path, 'r') as f: | ||
| tech_spec = f.read() | ||
|
|
||
| with open(test_cases_path, 'r') as f: | ||
| test_cases = f.read() | ||
|
|
||
| with open(metadata_path, 'r') as f: | ||
| metadata = json.load(f) | ||
|
|
||
| with open(html_path, 'r') as f: | ||
| html_content = f.read() | ||
|
|
||
| return tech_spec, test_cases, metadata, html_content | ||
|
|
||
| def read_existing_check(): | ||
| """Read the existing ValidZoneIdCheck stub.""" | ||
| check_path = Path("java-checks/src/main/java/org/sonar/java/checks/ValidZoneIdCheck.java") | ||
| with open(check_path, 'r') as f: | ||
| return f.read() | ||
|
|
||
| def find_similar_checks(): | ||
| """Find similar check implementations for reference.""" | ||
| checks = [ | ||
| "java-checks/src/main/java/org/sonar/java/checks/DateFormatWeekYearCheck.java", | ||
| "java-checks/src/main/java/org/sonar/java/checks/SimpleTemporalInstantiationCheck.java", | ||
| ] | ||
|
|
||
| similar_checks = {} | ||
| for check_path in checks: | ||
| path = Path(check_path) | ||
| if path.exists(): | ||
| with open(path, 'r') as f: | ||
| similar_checks[path.name] = f.read() | ||
|
|
||
| return similar_checks | ||
|
|
||
| def implement_check(client, tech_spec, test_cases, metadata, html_content, existing_check, similar_checks): | ||
| """Generate the Java check implementation using Claude API.""" | ||
|
|
||
| implementation_prompt = f"""You are tasked with implementing a SonarJava rule check based on the technical specification and test cases provided. | ||
|
|
||
| Rule: {metadata['title']} | ||
| Type: {metadata['type']} | ||
| Severity: {metadata['defaultSeverity']} | ||
| Key: S8265 | ||
|
|
||
| TECHNICAL SPECIFICATION: | ||
| {tech_spec} | ||
|
|
||
| TEST CASES FILE (ValidZoneIdCheckSample.java): | ||
| {test_cases} | ||
|
|
||
| EXISTING CHECK STUB: | ||
| {existing_check} | ||
|
|
||
| SIMILAR CHECK IMPLEMENTATIONS FOR REFERENCE: | ||
|
|
||
| DateFormatWeekYearCheck.java: | ||
| {similar_checks.get('DateFormatWeekYearCheck.java', 'Not available')} | ||
|
|
||
| SimpleTemporalInstantiationCheck.java: | ||
| {similar_checks.get('SimpleTemporalInstantiationCheck.java', 'Not available')} | ||
|
|
||
| Your task is to implement the ValidZoneIdCheck.java file based on the technical specification. The implementation should: | ||
|
|
||
| 1. **Extend AbstractMethodDetection** as specified in the technical specification | ||
| 2. **Use MethodMatchers** to detect ZoneId.of(String) calls | ||
| 3. **Validate string literals** against valid zone IDs using: | ||
| - ZoneId.getAvailableZoneIds() for IANA zone IDs | ||
| - Pattern matching for fixed offsets (+05:30, -08:00, etc.) | ||
| - Special identifiers (UTC, GMT, Z, UT) | ||
| - Zone offset IDs (UTC+1, GMT-5) | ||
| 4. **Only check string literals** using asConstant() to avoid false positives on dynamic strings | ||
| 5. **Report issues** with helpful messages suggesting alternatives for common mistakes | ||
| 6. **Follow SonarJava patterns** as seen in the similar checks provided | ||
|
|
||
| IMPORTANT IMPLEMENTATION DETAILS: | ||
| - Use AbstractMethodDetection as the base class (NOT IssuableSubscriptionVisitor) | ||
| - Override getMethodInvocationMatchers() to return the MethodMatchers for ZoneId.of(String) | ||
| - Override onMethodInvocationFound(MethodInvocationTree) to implement the detection logic | ||
| - Use argument.asConstant() to extract string literal values | ||
| - Cache VALID_ZONE_IDS in a static initializer for performance | ||
| - Include Levenshtein distance algorithm for suggesting corrections | ||
| - Add COMMON_CORRECTIONS map for three-letter abbreviations (PST -> America/Los_Angeles, etc.) | ||
|
|
||
| Generate the complete ValidZoneIdCheck.java file implementation. Output ONLY the Java code, no explanations or markdown formatting.""" | ||
|
|
||
| response = client.messages.create( | ||
| model="claude-sonnet-4-5-20250929", | ||
| max_tokens=8000, | ||
| messages=[{"role": "user", "content": implementation_prompt}] | ||
| ) | ||
|
|
||
| return response.content[0].text | ||
|
|
||
| def main(): | ||
| # Check for API configuration | ||
| api_key = os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("ANTHROPIC_AUTH_TOKEN") | ||
| if not api_key: | ||
| print("Error: ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN environment variable not set") | ||
| sys.exit(1) | ||
|
|
||
| # Use environment variables for base_url and custom headers if available | ||
| client_kwargs = {"api_key": api_key} | ||
| if base_url := os.environ.get("ANTHROPIC_BASE_URL"): | ||
| client_kwargs["base_url"] = base_url | ||
| if custom_headers := os.environ.get("ANTHROPIC_CUSTOM_HEADERS"): | ||
| # Parse custom headers from format "key: value" | ||
| headers = {} | ||
| for header in custom_headers.split(","): | ||
| if ":" in header: | ||
| k, v = header.split(":", 1) | ||
| headers[k.strip()] = v.strip() | ||
| if headers: | ||
| client_kwargs["default_headers"] = headers | ||
|
|
||
| client = Anthropic(**client_kwargs) | ||
|
|
||
| print("Loading planning files...") | ||
| tech_spec, test_cases, metadata, html_content = load_planning_files() | ||
|
|
||
| print("Reading existing check stub...") | ||
| existing_check = read_existing_check() | ||
|
|
||
| print("Finding similar check implementations...") | ||
| similar_checks = find_similar_checks() | ||
|
|
||
| print("Generating ValidZoneIdCheck implementation...") | ||
| implementation = implement_check(client, tech_spec, test_cases, metadata, html_content, existing_check, similar_checks) | ||
|
|
||
| # Clean up markdown code fences if present | ||
| implementation = implementation.strip() | ||
| if implementation.startswith('```java'): | ||
| implementation = implementation[7:] # Remove ```java | ||
| if implementation.startswith('```'): | ||
| implementation = implementation[3:] # Remove ``` | ||
| if implementation.endswith('```'): | ||
| implementation = implementation[:-3] # Remove closing ``` | ||
| implementation = implementation.strip() | ||
|
|
||
| # Save implementation | ||
| check_path = Path("java-checks/src/main/java/org/sonar/java/checks/ValidZoneIdCheck.java") | ||
| with open(check_path, 'w') as f: | ||
| f.write(implementation) | ||
| f.write('\n') # Ensure newline at end | ||
| print(f"Implementation saved to {check_path}") | ||
|
|
||
| print("\nImplement-rule complete!") | ||
| print(f"\nNext steps:") | ||
| print(f"1. Review the implementation in {check_path}") | ||
| print(f"2. Run tests: mvn test -Dtest=ValidZoneIdCheckTest") | ||
| print(f"3. Fix any compilation or test errors") | ||
| print(f"4. Register the check in CheckList.java") | ||
|
|
||
| if __name__ == "__main__": | ||
| main() |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Bug: create_pr.py prints empty PR URL when 'already exists' but lookup fails
In
create_pull_request, whengh pr createfails with returncode != 0 and stderr contains "already exists" butcheck_existing_prreturns 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 topr_url = result.stdout.strip(), which is empty on failure, and printsCreated draft PR:with no URL — silently reporting success despite no PR being created/found. Add an explicit return/exit in thealready existsbranch when no existing PR is found.Exit instead of falling through to an empty URL when the existing PR cannot be found.:
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎