presubmit: add location data to presubmit result

This will help us to display preusbmit result as findings both in
Gerrit and in Cider workspaces.

Change-Id: I0f5a9c503c1b14dc8dcbf9794cc556ef215a46a2
Bug: 404837554
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/6469939
Commit-Queue: Yiwei Zhang <yiwzhang@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
This commit is contained in:
Yiwei Zhang
2025-04-22 12:26:12 -07:00
committed by LUCI CQ
parent 5d891d2a8d
commit d49e17e19f
3 changed files with 339 additions and 34 deletions

View File

@@ -36,7 +36,8 @@ import unittest # Exposed through the API.
import urllib.parse as urlparse
import urllib.request as urllib_request
import urllib.error as urllib_error
from typing import Mapping
from dataclasses import asdict, dataclass
from typing import ClassVar, Mapping
from warnings import warn
# Local imports.
@@ -311,6 +312,67 @@ def prompt_should_continue(prompt_string):
return response in ('y', 'yes')
# Top level object so multiprocessing can pickle
# Public access through OutputApi object.
@dataclass
class _PresubmitResultLocation:
COMMIT_MSG_PATH: ClassVar[str] = '/COMMIT_MSG'
# path to the file where errors/warnings are reported.
#
# path MUST either be COMMIT_MSG_PATH or relative to the repo root to
# indicate the errors/warnings are against the commit message
# (a.k.a cl description).
file_path: str
# The range in the file defined by (start_line, start_col) -
# (end_line, end_col) where errors/warnings are reported.
# The semantic are the same as Gerrit comment range:
# https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-range
#
# To specify the entire line, make start_line == end_line and
# start_col == end_col == 0.
start_line: int = 0 # inclusive 1-based
start_col: int = 0 # inclusive 0-based
end_line: int = 0 # exclusive 1-based
end_col: int = 0 # exclusive 0-based
def validate(self):
if not self.file_path:
raise ValueError('file path is required')
if self.file_path != self.COMMIT_MSG_PATH and os.path.isabs(
self.file_path):
raise ValueError(
f'file path must be relative path, got {self.file_path}')
if not self.start_line:
if self.end_line:
raise ValueError('end_line must be empty if start line is not '
'specified')
if self.start_col:
raise ValueError('start_col must be empty if start line is not '
'specified')
if self.end_col:
raise ValueError('end_col must be empty if start line is not '
'specified')
elif self.start_line < 0:
raise ValueError('start_line MUST not be negative, '
f'got {self.start_line}')
elif self.end_line < 1:
raise ValueError('start_line is specified so end_line must be '
f'positive, got {self.end_line}')
elif self.start_col < 0:
raise ValueError('start_col MUST not be negative, '
f'got {self.start_col}')
elif self.end_col < 0:
raise ValueError('end_col MUST not be negative, '
f'got {self.end_col}')
elif self.start_line > self.end_line or (
self.start_line == self.end_line
and self.start_col > self.end_col and self.end_col > 0):
raise ValueError(
'(start_line, start_col) must not be after (end_line, end_col'
f'), got ({self.start_line}, {self.start_col}) .. '
f'({self.end_line}, {self.end_col})')
# Top level object so multiprocessing can pickle
# Public access through OutputApi object.
class _PresubmitResult(object):
@@ -318,15 +380,28 @@ class _PresubmitResult(object):
fatal = False
should_prompt = False
def __init__(self, message, items=None, long_text='', show_callstack=None):
"""
message: A short one-line message to indicate errors.
items: A list of short strings to indicate where errors occurred.
long_text: multi-line text output, e.g. from another tool
def __init__(self,
message: str,
items: list[str] = None,
long_text: str = '',
locations: list[_PresubmitResultLocation] = None,
show_callstack: bool = None):
"""Inits _PresubmitResult.
Args:
message: A short one-line message to indicate errors.
items: A list of short strings to indicate where errors occurred.
Note that if you are using this parameter to print where errors
occurred, please use `locations` instead
long_text: multi-line text output, e.g. from another tool
locations: The locations indicate where the errors occurred.
"""
self._message = _PresubmitResult._ensure_str(message)
self._items = items or []
self._long_text = _PresubmitResult._ensure_str(long_text.rstrip())
self._locations = locations or []
for loc in self._locations:
loc.validate()
if show_callstack is None:
show_callstack = _SHOW_CALLSTACKS
if show_callstack:
@@ -346,24 +421,45 @@ class _PresubmitResult(object):
return val.decode()
raise ValueError("Unknown string type %s" % type(val))
def handle(self):
sys.stdout.write(self._message)
sys.stdout.write('\n')
def handle(self, out_file=None):
if not out_file:
out_file = sys.stdout
out_file.write(self._message)
out_file.write('\n')
for item in self._items:
sys.stdout.write(' ')
out_file.write(' ')
# Write separately in case it's unicode.
sys.stdout.write(str(item))
sys.stdout.write('\n')
out_file.write(str(item))
out_file.write('\n')
if self._locations:
out_file.write('Found in:\n')
for loc in self._locations:
if loc.file_path == _PresubmitResultLocation.COMMIT_MSG_PATH:
out_file.write(' - Commit Message')
else:
out_file.write(f' - {loc.file_path}')
if not loc.start_line:
pass
elif loc.start_line == loc.end_line and (loc.start_col == 0
and loc.end_col == 0):
out_file.write(f' [Ln {loc.start_line}]')
elif loc.start_col == 0 and loc.end_col == 0:
out_file.write(f' [Ln {loc.start_line} - {loc.end_line}]')
else:
out_file.write(f' [Ln {loc.start_line}, Col {loc.start_col}'
f' - Ln {loc.end_line}, Col {loc.end_col}]')
out_file.write('\n')
if self._long_text:
sys.stdout.write('\n***************\n')
out_file.write('\n***************\n')
# Write separately in case it's unicode.
sys.stdout.write(self._long_text)
sys.stdout.write('\n***************\n')
out_file.write(self._long_text)
out_file.write('\n***************\n')
def json_format(self):
return {
'message': self._message,
'items': [str(item) for item in self._items],
'locations': [asdict(loc) for loc in self._locations],
'long_text': self._long_text,
'fatal': self.fatal
}
@@ -515,6 +611,7 @@ class OutputApi(object):
PresubmitPromptWarning = _PresubmitPromptWarning
PresubmitNotifyResult = _PresubmitNotifyResult
MailTextResult = _MailTextResult
PresubmitResultLocation = _PresubmitResultLocation
def __init__(self, is_committing):
self.is_committing = is_committing