diff --git a/.github/workflows/static-analysis.yaml b/.github/workflows/static-analysis.yaml index 2df6ed87ae3c..45eb836fadc7 100644 --- a/.github/workflows/static-analysis.yaml +++ b/.github/workflows/static-analysis.yaml @@ -102,6 +102,10 @@ jobs: BASE_SCORE=$(jq -r '.typeCompleteness.completenessScore' prefect-analysis-base.json) echo "base_score=$BASE_SCORE" >> $GITHUB_OUTPUT + - name: Checkout current branch + run: | + git checkout ${{ github.head_ref || github.ref_name }} + - name: Compare scores run: | CURRENT_SCORE=$(echo ${{ steps.calculate_current_score.outputs.current_score }}) @@ -110,6 +114,7 @@ jobs: if (( $(echo "$BASE_SCORE > $CURRENT_SCORE" | bc -l) )); then echo "❌ Type completeness score has decreased from $BASE_SCORE to $CURRENT_SCORE" >> $GITHUB_STEP_SUMMARY echo "Please add type annotations to your code to increase the type completeness score." >> $GITHUB_STEP_SUMMARY + uv run scripts/pyright_diff.py prefect-analysis-base.json prefect-analysis.json >> $GITHUB_STEP_SUMMARY exit 1 elif (( $(echo "$BASE_SCORE < $CURRENT_SCORE" | bc -l) )); then echo "✅ Type completeness score has increased from $BASE_SCORE to $CURRENT_SCORE" >> $GITHUB_STEP_SUMMARY diff --git a/scripts/pyright_diff.py b/scripts/pyright_diff.py new file mode 100644 index 000000000000..ceaf43a1601b --- /dev/null +++ b/scripts/pyright_diff.py @@ -0,0 +1,88 @@ +import json +import sys +from typing import Any, Dict, NamedTuple + + +class Diagnostic(NamedTuple): + """Structured representation of a diagnostic for easier table formatting.""" + + file: str + line: int + character: int + severity: str + message: str + + +def normalize_diagnostic(diagnostic: Dict[Any, Any]) -> Dict[Any, Any]: + """Normalize a diagnostic by removing or standardizing volatile fields.""" + normalized = diagnostic.copy() + normalized.pop("time", None) + normalized.pop("version", None) + return normalized + + +def load_and_normalize_file(file_path: str) -> Dict[Any, Any]: + """Load a JSON file and normalize its contents.""" + with open(file_path, "r") as f: + data = json.load(f) + return normalize_diagnostic(data) + + +def parse_diagnostic(diag: Dict[Any, Any]) -> Diagnostic: + """Convert a diagnostic dict into a Diagnostic object.""" + file = diag.get("file", "unknown_file") + message = diag.get("message", "no message") + range_info = diag.get("range", {}) + start = range_info.get("start", {}) + line = start.get("line", 0) + char = start.get("character", 0) + severity = diag.get("severity", "unknown") + + return Diagnostic(file, line, char, severity, message) + + +def format_markdown_table(diagnostics: list[Diagnostic]) -> str: + """Format list of diagnostics as a markdown table.""" + if not diagnostics: + return "\nNo new errors found!" + + table = ["| File | Location | Message |", "|------|----------|---------|"] + + for diag in sorted(diagnostics, key=lambda x: (x.file, x.line, x.character)): + # Escape pipe characters and replace newlines with HTML breaks + message = diag.message.replace("|", "\\|").replace("\n", "
") + location = f"L{diag.line}:{diag.character}" + table.append(f"| {diag.file} | {location} | {message} |") + + return "\n".join(table) + + +def compare_pyright_outputs(base_file: str, new_file: str) -> None: + """Compare two pyright JSON output files and display only new errors.""" + base_data = load_and_normalize_file(base_file) + new_data = load_and_normalize_file(new_file) + + # Group diagnostics by file + base_diags = set() + new_diags = set() + + # Process diagnostics from type completeness symbols + for data, diag_set in [(base_data, base_diags), (new_data, new_diags)]: + for symbol in data.get("typeCompleteness", {}).get("symbols", []): + for diag in symbol.get("diagnostics", []): + if diag.get("severity", "") == "error": + diag_set.add(parse_diagnostic(diag)) + + # Find new errors + new_errors = list(new_diags - base_diags) + + print("\n## New Pyright Errors\n") + print(format_markdown_table(new_errors)) + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python pyright_diff.py ") + sys.exit(1) + + compare_pyright_outputs(sys.argv[1], sys.argv[2])