servo/python/tidy/linting_report.py
Jerens Lensun 877010c5f6
Mach clippy & test-tidy github inline annotation (#37294)
When the --report-ci flag is passed to ./mach clippy or ./mach
test-tidy, the commands emit CI-friendly output to files in the tempy
directory. These files can later be used by [GitHub Workflow
Commands](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-an-error-message)
for annotations. If the flag is not provided, the default behavior
remains unchanged. Both clippy and test-tidy will limit have 10 limit
annotation

⚠️ Note: For ./mach clippy --report-ci to work correctly, the Clippy
command must use --message-format=json. If it's not specified, CI output
will not be generated, and no warning or error will be shown.

Example PR: https://github.com/jerensl/servo/pull/1/files

Fixes: #37231

---------

Signed-off-by: Jerens Lensun <jerensslensun@gmail.com>
2025-06-22 15:30:19 +00:00

114 lines
3.7 KiB
Python

# Copyright 2025 The Servo Project Developers. See the COPYRIGHT
# file at the top-level directory of this distribution.
#
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
# option. This file may not be copied, modified, or distributed
# except according to those terms.
from dataclasses import dataclass
from typing import Any, Literal, NotRequired
@dataclass
class GithubAnnotation:
file_name: str
line_start: int
line_end: int
level: Literal["notice", "warning", "error"]
title: str
message: str
column_start: NotRequired[int]
column_end: NotRequired[int]
class GitHubAnnotationManager:
def __init__(self, annotation_prefix: str, limit: int = 10):
self.annotation_prefix: str = annotation_prefix
self.limit: int = limit
self.total_count: int = 0
def clean_path(self, path: str):
return path.removeprefix("./")
def escape(self, s: str):
return s.replace("\r", "%0D").replace("\n", "%0A")
def emit_annotation(
self,
title: str,
message: str,
file_name: str,
line_start: int,
line_end: int | None = None,
annotation_level: str = "error",
column_start: int | None = None,
column_end: int | None = None,
):
if self.total_count >= self.limit:
return
if line_end is None:
line_end = line_start
annotation: GithubAnnotation = {
"title": f"{self.annotation_prefix}: {self.escape(title)}",
"message": self.escape(message),
"file_name": self.clean_path(file_name),
"line_start": line_start,
"line_end": line_end,
"level": annotation_level,
}
if line_start == line_end and column_start is not None and column_end is not None:
annotation["column_start"] = column_start
annotation["column_end"] = column_end
line_info = f"line={annotation['line_start']},endLine={annotation['line_end']},title={annotation['title']}"
column_info = ""
if "column_end" in annotation and "column_start" in annotation:
column_info = f"col={annotation['column_start']},endColumn={annotation['column_end']},"
print(
f"::{annotation['level']} file={annotation['file_name']},{column_info}{line_info}::{annotation['message']}"
)
self.total_count += 1
def emit_annotations_for_clippy(self, data: list[dict[str, Any]]):
severenty_map: dict[str, Literal["notice", "warning", "error"]] = {
"help": "notice",
"note": "notice",
"warning": "warning",
"error": "error",
}
for item in data:
if self.total_count >= self.limit:
break
message = item.get("message")
if not message:
continue
spans = message.get("spans") or []
primary_span = next((span for span in spans if span.get("is_primary")), None)
if not primary_span:
continue
annotation_level = severenty_map.get(message.get("level"), "error")
title = message.get("message", "")
rendered_message = message.get("rendered", "")
self.emit_annotation(
title,
rendered_message,
primary_span["file_name"],
primary_span["line_start"],
primary_span["line_end"],
annotation_level,
primary_span["column_start"],
primary_span["column_end"],
)