Initial commit
This commit is contained in:
144
skills/recipe-writer/scripts/add_license_header.sh
Executable file
144
skills/recipe-writer/scripts/add_license_header.sh
Executable 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
|
||||
299
skills/recipe-writer/scripts/init_recipe.py
Executable file
299
skills/recipe-writer/scripts/init_recipe.py
Executable 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()
|
||||
331
skills/recipe-writer/scripts/validate_recipe.py
Executable file
331
skills/recipe-writer/scripts/validate_recipe.py
Executable 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()
|
||||
Reference in New Issue
Block a user