Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:57:41 +08:00
commit c2d0b101b0
22 changed files with 6446 additions and 0 deletions

View File

@@ -0,0 +1,144 @@
#!/usr/bin/env bash
#
# License Header Script
#
# Adds or updates license headers in Java source files based on the project's
# license template file (gradle/licenseHeader.txt).
#
# Usage:
# ./add_license_header.sh <java-file>
# ./add_license_header.sh src/main/java/com/example/MyRecipe.java
#
# Features:
# - Checks for gradle/licenseHeader.txt in repository root
# - Substitutes ${year} with current year
# - Preserves existing package/import statements
# - Skips files that already have the correct header
#
# Exit codes:
# 0 - Success (header added or already present)
# 1 - License template file not found
# 2 - Invalid arguments or file not found
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Get current year
CURRENT_YEAR=$(date +%Y)
# Function to find repository root
find_repo_root() {
local dir="$PWD"
while [[ "$dir" != "/" ]]; do
if [[ -d "$dir/.git" ]] || [[ -f "$dir/build.gradle" ]] || [[ -f "$dir/build.gradle.kts" ]]; then
echo "$dir"
return 0
fi
dir=$(dirname "$dir")
done
echo "$PWD"
return 1
}
# Function to show usage
usage() {
echo "Usage: $0 <java-file>"
echo ""
echo "Add or update license header in a Java source file."
echo ""
echo "Example:"
echo " $0 src/main/java/com/example/MyRecipe.java"
exit 2
}
# Check arguments
if [[ $# -ne 1 ]]; then
usage
fi
JAVA_FILE="$1"
# Validate Java file exists
if [[ ! -f "$JAVA_FILE" ]]; then
echo -e "${RED}Error: File not found: $JAVA_FILE${NC}" >&2
exit 2
fi
# Validate it's a Java file
if [[ ! "$JAVA_FILE" =~ \.java$ ]]; then
echo -e "${RED}Error: Not a Java file: $JAVA_FILE${NC}" >&2
exit 2
fi
# Find repository root and license header file
REPO_ROOT=$(find_repo_root)
LICENSE_HEADER_FILE="$REPO_ROOT/gradle/licenseHeader.txt"
# Check if license header template exists
if [[ ! -f "$LICENSE_HEADER_FILE" ]]; then
echo -e "${YELLOW}Warning: License header template not found at: $LICENSE_HEADER_FILE${NC}" >&2
echo -e "${YELLOW}Skipping license header addition.${NC}" >&2
exit 1
fi
# Read license header template and substitute ${year}
LICENSE_HEADER=$(sed "s/\${year}/$CURRENT_YEAR/g" "$LICENSE_HEADER_FILE")
# Create a temporary file for the new content
TEMP_FILE=$(mktemp)
trap "rm -f $TEMP_FILE" EXIT
# Check if file already has a license header (starts with /* or //)
FIRST_LINE=$(head -n 1 "$JAVA_FILE")
if [[ "$FIRST_LINE" =~ ^/\* ]] || [[ "$FIRST_LINE" =~ ^// ]]; then
# File has some kind of header comment
# Extract everything after the header comment
# Find the end of the comment block
if [[ "$FIRST_LINE" =~ ^/\* ]]; then
# Multi-line comment - find the closing */
LINE_NUM=$(grep -n "\*/" "$JAVA_FILE" | head -n 1 | cut -d: -f1)
if [[ -n "$LINE_NUM" ]]; then
# Extract content after the comment block
tail -n +$((LINE_NUM + 1)) "$JAVA_FILE" > "$TEMP_FILE.body"
else
# No closing found, treat whole file as body
cp "$JAVA_FILE" "$TEMP_FILE.body"
fi
else
# Single-line comments - skip all leading // lines
LINE_NUM=$(grep -n -m 1 "^[^/]" "$JAVA_FILE" | head -n 1 | cut -d: -f1)
if [[ -n "$LINE_NUM" ]]; then
tail -n +$LINE_NUM "$JAVA_FILE" > "$TEMP_FILE.body"
else
# All lines are comments, preserve the file
cp "$JAVA_FILE" "$TEMP_FILE.body"
fi
fi
# Write new header + body
echo "$LICENSE_HEADER" > "$TEMP_FILE"
echo "" >> "$TEMP_FILE" # Add blank line after header
cat "$TEMP_FILE.body" >> "$TEMP_FILE"
rm -f "$TEMP_FILE.body"
echo -e "${GREEN}✓ Updated license header in: $JAVA_FILE${NC}"
else
# No header comment found, prepend the license header
echo "$LICENSE_HEADER" > "$TEMP_FILE"
echo "" >> "$TEMP_FILE" # Add blank line after header
cat "$JAVA_FILE" >> "$TEMP_FILE"
echo -e "${GREEN}✓ Added license header to: $JAVA_FILE${NC}"
fi
# Replace original file with new content
mv "$TEMP_FILE" "$JAVA_FILE"
exit 0

View File

@@ -0,0 +1,299 @@
#!/usr/bin/env python3
"""
OpenRewrite Recipe Initialization Script
Generates boilerplate code for new OpenRewrite YAML recipes including:
- Recipe class file
- Test file
- Optional declarative YAML recipe
Usage:
python init_recipe.py --name MyRecipe --package com.example.rewrite --description "Recipe description"
"""
import argparse
import os
import sys
from datetime import datetime
from pathlib import Path
def read_license_header():
"""Read license header from gradle/licenseHeader.txt if it exists."""
license_path = Path.cwd()
while license_path != license_path.parent:
license_file = license_path / "gradle" / "licenseHeader.txt"
if license_file.exists():
with open(license_file, 'r') as f:
content = f.read()
# Substitute ${year} with current year
content = content.replace("${year}", str(datetime.now().year))
return content + "\n"
license_path = license_path.parent
return ""
def to_snake_case(name):
"""Convert PascalCase to snake_case."""
result = []
for i, char in enumerate(name):
if char.isupper() and i > 0:
result.append('_')
result.append(char.lower())
return ''.join(result)
def generate_recipe_class(name, package, description, license_header):
"""Generate the recipe class file content."""
return f"""{license_header}package {package};
import org.openrewrite.*;
import org.openrewrite.yaml.YamlIsoVisitor;
import org.openrewrite.yaml.tree.Yaml;
public class {name} extends Recipe {{
@Override
public String getDisplayName() {{
return "{name}";
}}
@Override
public String getDescription() {{
return "{description}";
}}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {{
return new YamlIsoVisitor<ExecutionContext>() {{
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {{
// TODO: Implement recipe logic
return super.visitMappingEntry(entry, ctx);
}}
}};
}}
}}
"""
def generate_parameterized_recipe_class(name, package, description, license_header):
"""Generate a parameterized recipe class file content."""
return f"""{license_header}package {package};
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.*;
import org.openrewrite.yaml.YamlIsoVisitor;
import org.openrewrite.yaml.tree.Yaml;
@Value
@EqualsAndHashCode(callSuper = false)
public class {name} extends Recipe {{
@Option(
displayName = "Parameter name",
description = "Description of the parameter",
example = "example-value"
)
String parameterName;
@Override
public String getDisplayName() {{
return "{name}";
}}
@Override
public String getDescription() {{
return "{description}";
}}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {{
return new YamlIsoVisitor<ExecutionContext>() {{
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {{
// TODO: Implement recipe logic using parameterName
return super.visitMappingEntry(entry, ctx);
}}
}};
}}
}}
"""
def generate_test_class(name, package, license_header):
"""Generate the test class file content."""
return f"""{license_header}package {package};
import org.junit.jupiter.api.Test;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;
import static org.openrewrite.yaml.Assertions.yaml;
class {name}Test implements RewriteTest {{
@Override
public void defaults(RecipeSpec spec) {{
spec.recipe(new {name}());
}}
@Test
void basicTransformation() {{
rewriteRun(
yaml(
\"\"\"
# Before YAML
key: old-value
\"\"\",
\"\"\"
# After YAML
key: new-value
\"\"\"
)
);
}}
@Test
void doesNotChangeUnrelatedYaml() {{
rewriteRun(
yaml(
\"\"\"
unrelated: value
\"\"\"
)
);
}}
@Test
void handlesEdgeCases() {{
rewriteRun(
yaml(
\"\"\"
# Empty value
key:
# Null value
key2: null
\"\"\"
)
);
}}
}}
"""
def generate_declarative_recipe(name, package, description):
"""Generate declarative YAML recipe content."""
return f"""---
type: specs.openrewrite.org/v1beta/recipe
name: {package}.{name}
displayName: {name}
description: {description}
recipeList:
- org.openrewrite.yaml.search.FindKey:
keyPath: $.some.path
- org.openrewrite.yaml.ChangeValue:
keyPath: $.some.path
value: newValue
"""
def create_file(path, content):
"""Create a file with the given content."""
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'w') as f:
f.write(content)
print(f"Created: {path}")
def main():
parser = argparse.ArgumentParser(
description="Initialize a new OpenRewrite YAML recipe with boilerplate code"
)
parser.add_argument(
"--name",
required=True,
help="Recipe class name (PascalCase, e.g., UpdateGitHubActions)"
)
parser.add_argument(
"--package",
required=True,
help="Package name (e.g., com.example.rewrite)"
)
parser.add_argument(
"--description",
required=True,
help="Recipe description"
)
parser.add_argument(
"--parameterized",
action="store_true",
help="Generate parameterized recipe with @Option annotation"
)
parser.add_argument(
"--declarative",
action="store_true",
help="Also generate declarative YAML recipe template"
)
parser.add_argument(
"--output-dir",
default=".",
help="Output directory (default: current directory)"
)
args = parser.parse_args()
# Validate recipe name
if not args.name[0].isupper():
print("Error: Recipe name must start with uppercase letter (PascalCase)")
sys.exit(1)
# Read license header
license_header = read_license_header()
if license_header:
print(f"Found license header (will use year {datetime.now().year})")
# Convert package to path
package_path = args.package.replace('.', '/')
base_path = Path(args.output_dir)
# Generate file paths
recipe_path = base_path / "src" / "main" / "java" / package_path / f"{args.name}.java"
test_path = base_path / "src" / "test" / "java" / package_path / f"{args.name}Test.java"
# Generate recipe class
if args.parameterized:
recipe_content = generate_parameterized_recipe_class(
args.name, args.package, args.description, license_header
)
else:
recipe_content = generate_recipe_class(
args.name, args.package, args.description, license_header
)
# Generate test class
test_content = generate_test_class(args.name, args.package, license_header)
# Create files
create_file(recipe_path, recipe_content)
create_file(test_path, test_content)
# Generate declarative recipe if requested
if args.declarative:
yaml_name = to_snake_case(args.name)
yaml_path = base_path / "src" / "main" / "resources" / "META-INF" / "rewrite" / f"{yaml_name}.yml"
yaml_content = generate_declarative_recipe(args.name, args.package, args.description)
create_file(yaml_path, yaml_content)
print("\n✓ Recipe initialization complete!")
print("\nNext steps:")
print("1. Write failing tests in the test file")
print("2. Implement the recipe logic")
print("3. Run tests to verify: ./gradlew test")
if args.declarative:
print("4. Choose between imperative (Java) or declarative (YAML) approach")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,331 @@
#!/usr/bin/env python3
"""
Validate OpenRewrite recipe structure, naming conventions, and Java compatibility.
This script checks:
1. Recipe class structure (extends Recipe, has required annotations)
2. Required methods (getDisplayName, getDescription, getVisitor)
3. Naming conventions (package, class name, display name)
4. Java compatibility (can compile with Java 8)
5. YAML recipe format (for declarative recipes)
Usage:
python validate_recipe.py <path-to-recipe>
python validate_recipe.py <path-to-recipe> --java-version 8
python validate_recipe.py <path-to-recipe> --no-compile
"""
import argparse
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import List, Tuple
class Colors:
"""ANSI color codes for terminal output"""
GREEN = '\033[92m'
RED = '\033[91m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
RESET = '\033[0m'
BOLD = '\033[1m'
def print_success(message: str):
print(f"{Colors.GREEN}{Colors.RESET} {message}")
def print_error(message: str):
print(f"{Colors.RED}{Colors.RESET} {message}")
def print_warning(message: str):
print(f"{Colors.YELLOW}{Colors.RESET} {message}")
def print_info(message: str):
print(f"{Colors.BLUE}{Colors.RESET} {message}")
def check_java_file_structure(content: str, file_path: Path) -> List[str]:
"""Validate Java recipe file structure"""
errors = []
# Check for Recipe class
if not re.search(r'class\s+\w+\s+extends\s+Recipe', content):
errors.append("Recipe class must extend Recipe")
# Check for @Value annotation (for immutability)
if '@Value' not in content:
print_warning(f"Consider using @Value annotation for immutability")
# Check for @EqualsAndHashCode
if '@EqualsAndHashCode' not in content:
print_warning("Consider using @EqualsAndHashCode(callSuper = false)")
# Check for required methods
if 'getDisplayName()' not in content:
errors.append("Recipe must override getDisplayName()")
if 'getDescription()' not in content:
errors.append("Recipe must override getDescription()")
if 'getVisitor()' not in content:
errors.append("Recipe must override getVisitor()")
# Check for proper return in getVisitor
if 'getVisitor()' in content:
visitor_match = re.search(r'public\s+TreeVisitor<\?.*?>\s+getVisitor\([^)]*\)\s*{([^}]+)}', content, re.DOTALL)
if visitor_match:
visitor_body = visitor_match.group(1)
if 'new ' not in visitor_body:
print_warning("getVisitor() should return a NEW instance (no caching)")
# Check display name ends with period
display_name_match = re.search(r'getDisplayName\(\)\s*{\s*return\s*"([^"]+)"', content)
if display_name_match:
display_name = display_name_match.group(1)
if not display_name.endswith('.') and not display_name.endswith('!') and not display_name.endswith('?'):
print_warning(f"Display name should end with a period: '{display_name}'")
# Check for @Option annotations on parameters
option_count = content.count('@Option')
if option_count > 0:
# Check that options have example
for match in re.finditer(r'@Option\([^)]+\)', content):
option = match.group(0)
if 'example' not in option:
print_warning("@Option should include an example parameter")
return errors
def check_naming_conventions(content: str, file_path: Path) -> List[str]:
"""Check naming conventions"""
errors = []
# Extract package name
package_match = re.search(r'package\s+([\w.]+);', content)
if package_match:
package = package_match.group(1)
if package.startswith('com.yourorg') or package.startswith('com.example'):
print_warning(f"Update placeholder package name: {package}")
# Extract class name
class_match = re.search(r'class\s+(\w+)\s+extends\s+Recipe', content)
if class_match:
class_name = class_match.group(1)
# Check naming convention (VerbNoun pattern)
if not re.match(r'^[A-Z][a-z]+[A-Z]', class_name):
print_warning(f"Recipe class name should follow VerbNoun pattern: {class_name}")
# Check file name matches class name
expected_filename = f"{class_name}.java"
if file_path.name != expected_filename:
errors.append(f"File name {file_path.name} does not match class name {expected_filename}")
return errors
def check_java8_compatibility_patterns(content: str) -> List[str]:
"""Check for Java 8 incompatible patterns"""
warnings = []
# Check for var keyword
if re.search(r'\bvar\b', content):
warnings.append("Found 'var' keyword - not available in Java 8")
# Check for text blocks
if '"""' in content:
warnings.append("Found text blocks (triple quotes) - not available in Java 8")
# Check for switch expressions
if re.search(r'switch\s*\([^)]+\)\s*{[^}]*->', content):
warnings.append("Found switch expression - not available in Java 8")
# Check for pattern matching
if re.search(r'instanceof\s+\w+\s+\w+\s+&&', content):
warnings.append("Found pattern matching in instanceof - not available in Java 8")
# Check for record keyword
if re.search(r'\brecord\s+\w+', content):
warnings.append("Found record - not available in Java 8")
return warnings
def compile_with_javac(file_path: Path, java_version: int = 8) -> Tuple[bool, str]:
"""Try to compile the file with javac"""
try:
# Create a temporary directory for compilation
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
result = subprocess.run(
['javac', '-source', str(java_version), '-target', str(java_version),
'-d', tmpdir, str(file_path)],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
return True, ""
else:
return False, result.stderr
except FileNotFoundError:
return False, "javac not found in PATH"
except subprocess.TimeoutExpired:
return False, "Compilation timed out"
except Exception as e:
return False, str(e)
def validate_yaml_recipe(content: str, file_path: Path) -> List[str]:
"""Validate YAML recipe format"""
errors = []
# Check for required fields
if 'type: specs.openrewrite.org/v1beta/recipe' not in content:
errors.append("YAML recipe must have 'type: specs.openrewrite.org/v1beta/recipe'")
if not re.search(r'^name:\s+[\w.]+', content, re.MULTILINE):
errors.append("YAML recipe must have 'name' field")
if not re.search(r'^displayName:', content, re.MULTILINE):
errors.append("YAML recipe must have 'displayName' field")
if not re.search(r'^description:', content, re.MULTILINE):
errors.append("YAML recipe must have 'description' field")
if not re.search(r'^recipeList:', content, re.MULTILINE):
errors.append("YAML recipe must have 'recipeList' field")
# Check naming convention
name_match = re.search(r'^name:\s+([\w.]+)', content, re.MULTILINE)
if name_match:
name = name_match.group(1)
if not re.match(r'^[\w.]+\.[\w.]+$', name):
print_warning(f"Recipe name should be fully qualified: {name}")
if 'yourorg' in name.lower() or 'example' in name.lower():
print_warning(f"Update placeholder recipe name: {name}")
return errors
def validate_recipe(file_path: Path, java_version: int = 8, skip_compile: bool = False) -> bool:
"""Validate a recipe file"""
print(f"\n{Colors.BOLD}Validating: {file_path}{Colors.RESET}\n")
if not file_path.exists():
print_error(f"File not found: {file_path}")
return False
# Read file content
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
print_error(f"Error reading file: {e}")
return False
errors = []
warnings = []
# Determine file type
if file_path.suffix == '.java':
print_info("Checking Java recipe structure...")
errors.extend(check_java_file_structure(content, file_path))
print_info("Checking naming conventions...")
errors.extend(check_naming_conventions(content, file_path))
print_info(f"Checking Java {java_version} compatibility...")
java8_warnings = check_java8_compatibility_patterns(content)
warnings.extend(java8_warnings)
# Try to compile if not skipped
if not skip_compile:
print_info("Attempting compilation...")
success, compile_error = compile_with_javac(file_path, java_version)
if not success:
if "javac not found" in compile_error:
print_warning("javac not found - skipping compilation check")
else:
errors.append(f"Compilation failed:\n{compile_error}")
else:
print_success("Compilation successful")
elif file_path.suffix in ['.yml', '.yaml']:
print_info("Checking YAML recipe format...")
errors.extend(validate_yaml_recipe(content, file_path))
else:
print_error(f"Unsupported file type: {file_path.suffix}")
return False
# Print results
print()
if errors:
print(f"{Colors.RED}{Colors.BOLD}ERRORS:{Colors.RESET}")
for error in errors:
print_error(error)
print()
if warnings:
print(f"{Colors.YELLOW}{Colors.BOLD}WARNINGS:{Colors.RESET}")
for warning in warnings:
print_warning(warning)
print()
if not errors and not warnings:
print(f"{Colors.GREEN}{Colors.BOLD}✓ All checks passed!{Colors.RESET}\n")
return True
elif not errors:
print(f"{Colors.YELLOW}{Colors.BOLD}⚠ Validation passed with warnings{Colors.RESET}\n")
return True
else:
print(f"{Colors.RED}{Colors.BOLD}✗ Validation failed{Colors.RESET}\n")
return False
def main():
parser = argparse.ArgumentParser(
description='Validate OpenRewrite recipe files',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
# Validate a Java recipe
python validate_recipe.py src/main/java/com/example/MyRecipe.java
# Validate with Java 11
python validate_recipe.py MyRecipe.java --java-version 11
# Skip compilation check
python validate_recipe.py MyRecipe.java --no-compile
# Validate a YAML recipe
python validate_recipe.py src/main/resources/META-INF/rewrite/my-recipe.yml
'''
)
parser.add_argument('path', type=str, help='Path to recipe file')
parser.add_argument('--java-version', type=int, default=8,
help='Target Java version (default: 8)')
parser.add_argument('--no-compile', action='store_true',
help='Skip compilation check')
args = parser.parse_args()
file_path = Path(args.path)
success = validate_recipe(file_path, args.java_version, args.no_compile)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()