|
1 |
| -"""The Base module of the :mod:`cpp_linter` package. This holds the objects shared by |
2 |
| -multiple modules.""" |
3 |
| -import os |
4 |
| -from pathlib import Path |
| 1 | +"""Run clang-tidy and clang-format on a list of files. |
| 2 | +If executed from command-line, then `main()` is the entrypoint. |
| 3 | +""" |
| 4 | +import json |
5 | 5 | import logging
|
6 |
| -import platform |
7 |
| -from typing import TYPE_CHECKING, List, Dict, Tuple, Any, Union, Optional |
8 |
| -import shutil |
9 |
| -from requests import Response |
10 |
| - |
11 |
| -if TYPE_CHECKING: # Used to avoid circular imports |
12 |
| - from cpp_linter.clang_format_xml import XMLFixit # noqa: F401 |
13 |
| - from cpp_linter.clang_tidy_yml import YMLFixit # noqa: F401 |
14 |
| - from cpp_linter.clang_tidy import TidyNotification # noqa: F401 |
15 |
| - |
16 |
| -FOUND_RICH_LIB = False |
17 |
| -try: |
18 |
| - from rich.logging import RichHandler |
19 |
| - |
20 |
| - FOUND_RICH_LIB = True |
21 |
| - |
22 |
| - logging.basicConfig( |
23 |
| - format="%(name)s: %(message)s", |
24 |
| - handlers=[RichHandler(show_time=False)], |
25 |
| - ) |
| 6 | +import os |
| 7 | +from .common_fs import list_source_files, CACHE_PATH |
| 8 | +from .loggers import start_log_group, end_log_group, logger |
| 9 | +from .clang_tools import capture_clang_tools_output |
| 10 | +from .cli import cli_arg_parser, parse_ignore_option |
| 11 | +from .rest_api.github_api import GithubApiClient |
26 | 12 |
|
27 |
| -except ImportError: # pragma: no cover |
28 |
| - logging.basicConfig() |
29 | 13 |
|
30 |
| -#: The :py:class:`logging.Logger` object used for outputting data. |
31 |
| -logger = logging.getLogger("CPP Linter") |
32 |
| -if not FOUND_RICH_LIB: |
33 |
| - logger.debug("rich module not found") |
| 14 | +def main(): |
| 15 | + """The main script.""" |
34 | 16 |
|
35 |
| -# global constant variables |
36 |
| -IS_ON_RUNNER = bool(os.getenv("CI")) |
37 |
| -GITHUB_SHA = os.getenv("GITHUB_SHA", "") |
38 |
| -GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", os.getenv("GIT_REST_API", "")) |
39 |
| -IS_ON_WINDOWS = platform.system().lower() == "windows" |
40 |
| -CACHE_PATH = Path(os.getenv("CPP_LINTER_CACHE", ".cpp-linter_cache")) |
41 |
| -CLANG_FORMAT_XML = CACHE_PATH / "clang_format_output.xml" |
42 |
| -CLANG_TIDY_YML = CACHE_PATH / "clang_tidy_output.yml" |
43 |
| -CLANG_TIDY_STDOUT = CACHE_PATH / "clang_tidy_report.txt" |
44 |
| -CHANGED_FILES_JSON = CACHE_PATH / "changed_files.json" |
| 17 | + # The parsed CLI args |
| 18 | + args = cli_arg_parser.parse_args() |
45 | 19 |
|
| 20 | + # force files-changed-only to reflect value of lines-changed-only |
| 21 | + if args.lines_changed_only: |
| 22 | + args.files_changed_only = True |
46 | 23 |
|
47 |
| -def make_headers(use_diff: bool = False) -> Dict[str, str]: |
48 |
| - """Create a `dict` for use in REST API headers. |
| 24 | + rest_api_client = GithubApiClient() |
| 25 | + logger.info("processing %s event", rest_api_client.event_name) |
49 | 26 |
|
50 |
| - :param use_diff: A flag to indicate that the returned format should be in diff |
51 |
| - syntax. |
52 |
| - :returns: A `dict` to be used as headers in `requests` API calls. |
53 |
| - """ |
54 |
| - headers = { |
55 |
| - "Accept": "application/vnd.github." + ("diff" if use_diff else "text+json"), |
56 |
| - } |
57 |
| - if GITHUB_TOKEN: |
58 |
| - headers["Authorization"] = f"token {GITHUB_TOKEN}" |
59 |
| - return headers |
| 27 | + # set logging verbosity |
| 28 | + logger.setLevel(10 if args.verbosity or rest_api_client.debug_enabled else 20) |
60 | 29 |
|
| 30 | + # prepare ignored paths list |
| 31 | + ignored, not_ignored = parse_ignore_option(args.ignore, args.files) |
61 | 32 |
|
62 |
| -class FileObj: |
63 |
| - """A class to represent a single file being analyzed. |
| 33 | + # change working directory |
| 34 | + os.chdir(args.repo_root) |
| 35 | + CACHE_PATH.mkdir(exist_ok=True) |
64 | 36 |
|
65 |
| - :param name: The file name. This should use Unix style path delimiters (``/``), |
66 |
| - even on Windows. |
67 |
| - :param additions: A `list` of line numbers that have added changes in the diff. |
68 |
| - This value is used to populate the `lines_added` property. |
69 |
| - :param diff_chunks: The ranges that define the beginning and ending line numbers |
70 |
| - for all hunks in the diff. |
71 |
| - """ |
| 37 | + if logger.getEffectiveLevel() <= logging.DEBUG: |
| 38 | + start_log_group("Event json from the runner") |
| 39 | + logger.debug(json.dumps(rest_api_client.event_payload)) |
| 40 | + end_log_group() |
72 | 41 |
|
73 |
| - def __init__(self, name: str, additions: List[int], diff_chunks: List[List[int]]): |
74 |
| - self.name: str = name #: The file name |
75 |
| - self.additions: List[int] = additions |
76 |
| - """A list of line numbers that contain added changes. This will be empty if |
77 |
| - not focusing on lines changed only.""" |
78 |
| - self.diff_chunks: List[List[int]] = diff_chunks |
79 |
| - """A list of line numbers that define the beginning and ending of hunks in the |
80 |
| - diff. This will be empty if not focusing on lines changed only.""" |
81 |
| - self.lines_added: List[List[int]] = FileObj._consolidate_list_to_ranges( |
82 |
| - additions |
| 42 | + if args.files_changed_only: |
| 43 | + files = rest_api_client.get_list_of_changed_files( |
| 44 | + args.extensions, |
| 45 | + ignored, |
| 46 | + not_ignored, |
| 47 | + args.lines_changed_only, |
83 | 48 | )
|
84 |
| - """A list of line numbers that define the beginning and ending of ranges that |
85 |
| - have added changes. This will be empty if not focusing on lines changed only. |
86 |
| - """ |
87 |
| - |
88 |
| - @staticmethod |
89 |
| - def _consolidate_list_to_ranges(numbers: List[int]) -> List[List[int]]: |
90 |
| - """A helper function that is only used after parsing the lines from a diff that |
91 |
| - contain additions. |
92 |
| -
|
93 |
| - :param numbers: A `list` of integers representing the lines' numbers that |
94 |
| - contain additions. |
95 |
| - :returns: A consolidated sequence of lists. Each list will have 2 items |
96 |
| - describing the starting and ending lines of all line ``numbers``. |
97 |
| - """ |
98 |
| - result: List[List[int]] = [] |
99 |
| - for i, n in enumerate(numbers): |
100 |
| - if not i: |
101 |
| - result.append([n]) |
102 |
| - elif n - 1 != numbers[i - 1]: |
103 |
| - result[-1].append(numbers[i - 1] + 1) |
104 |
| - result.append([n]) |
105 |
| - if i == len(numbers) - 1: |
106 |
| - result[-1].append(n + 1) |
107 |
| - return result |
108 |
| - |
109 |
| - def range_of_changed_lines( |
110 |
| - self, lines_changed_only: int, get_ranges: bool = False |
111 |
| - ) -> Union[List[int], List[List[int]]]: |
112 |
| - """Assemble a list of lines changed. |
113 |
| -
|
114 |
| - :param lines_changed_only: A flag to indicate the focus of certain lines. |
115 |
| -
|
116 |
| - - ``0``: focuses on all lines in a file(s). |
117 |
| - - ``1``: focuses on any lines shown in the event's diff (may include |
118 |
| - unchanged lines). |
119 |
| - - ``2``: focuses strictly on lines in the diff that contain additions. |
120 |
| - :param get_ranges: A flag to return a list of sequences representing |
121 |
| - :py:class:`range` parameters. Defaults to `False` since this is only |
122 |
| - required when constructing clang-tidy or clang-format CLI arguments. |
123 |
| - :returns: |
124 |
| - A list of line numbers for which to give attention. If ``get_ranges`` is |
125 |
| - asserted, then the returned list will be a list of ranges. If |
126 |
| - ``lines_changed_only`` is ``0``, then an empty list is returned. |
127 |
| - """ |
128 |
| - if lines_changed_only: |
129 |
| - ranges = self.diff_chunks if lines_changed_only == 1 else self.lines_added |
130 |
| - if get_ranges: |
131 |
| - return ranges |
132 |
| - return self.additions |
133 |
| - # we return an empty list (instead of None) here so we can still iterate it |
134 |
| - return [] # type: ignore[return-value] |
135 |
| - |
136 |
| - def serialize(self) -> Dict[str, Any]: |
137 |
| - """For easy debugging, use this method to serialize the `FileObj` into a json |
138 |
| - compatible `dict`.""" |
139 |
| - return { |
140 |
| - "filename": self.name, |
141 |
| - "line_filter": { |
142 |
| - "diff_chunks": self.diff_chunks, |
143 |
| - "lines_added": self.lines_added, |
144 |
| - }, |
145 |
| - } |
146 |
| - |
147 |
| - |
148 |
| -class Globals: |
149 |
| - """Global variables for re-use (non-constant).""" |
150 |
| - |
151 |
| - TIDY_COMMENT: str = "" |
152 |
| - """The accumulated output of clang-tidy (gets appended to OUTPUT)""" |
153 |
| - FORMAT_COMMENT: str = "" |
154 |
| - OUTPUT: str = "<!-- cpp linter action -->\n# Cpp-Linter Report " |
155 |
| - """The accumulated body of the resulting comment that gets posted.""" |
156 |
| - FILES: List[FileObj] = [] |
157 |
| - """The responding payload containing info about changed files.""" |
158 |
| - EVENT_PAYLOAD: Dict[str, Any] = {} |
159 |
| - """The parsed JSON of the event payload.""" |
160 |
| - response_buffer: Response = Response() |
161 |
| - """A shared response object for `requests` module.""" |
162 |
| - format_failed_count: int = 0 |
163 |
| - """A total count of clang-format concerns""" |
164 |
| - tidy_failed_count: int = 0 |
165 |
| - """A total count of clang-tidy concerns""" |
166 |
| - |
167 |
| - |
168 |
| -class GlobalParser: |
169 |
| - """Global variables specific to output parsers. Each element in each of the |
170 |
| - following attributes represents a clang-tool's output for 1 source file. |
171 |
| - """ |
172 |
| - |
173 |
| - tidy_notes = [] # type: List[TidyNotification] |
174 |
| - """This can only be a `list` of type |
175 |
| - :class:`~cpp_linter.clang_tidy.TidyNotification`.""" |
176 |
| - tidy_advice = [] # type: List[YMLFixit] |
177 |
| - """This can only be a `list` of type :class:`~cpp_linter.clang_tidy_yml.YMLFixit`. |
178 |
| - """ |
179 |
| - format_advice = [] # type: List[XMLFixit] |
180 |
| - """This can only be a `list` of type :class:`~cpp_linter.clang_format_xml.XMLFixit`. |
181 |
| - """ |
182 |
| - |
183 |
| - |
184 |
| -def get_line_cnt_from_cols(file_path: str, offset: int) -> Tuple[int, int]: |
185 |
| - """Gets a line count and columns offset from a file's absolute offset. |
186 |
| -
|
187 |
| - :param file_path: Path to file. |
188 |
| - :param offset: The byte offset to translate |
189 |
| -
|
190 |
| - :returns: |
191 |
| - A `tuple` of 2 `int` numbers: |
192 |
| -
|
193 |
| - - Index 0 is the line number for the given offset. |
194 |
| - - Index 1 is the column number for the given offset on the line. |
195 |
| - """ |
196 |
| - # logger.debug("Getting line count from %s at offset %d", file_path, offset) |
197 |
| - contents = Path(file_path).read_bytes()[:offset] |
198 |
| - return (contents.count(b"\n") + 1, offset - contents.rfind(b"\n")) |
199 |
| - |
200 |
| - |
201 |
| -def log_response_msg() -> bool: |
202 |
| - """Output the response buffer's message on a failed request. |
203 |
| -
|
204 |
| - :returns: |
205 |
| - A bool describing if response's status code was less than 400. |
206 |
| - """ |
207 |
| - if Globals.response_buffer.status_code >= 400: |
208 |
| - logger.error( |
209 |
| - "response returned %d message: %s", |
210 |
| - Globals.response_buffer.status_code, |
211 |
| - Globals.response_buffer.text, |
| 49 | + if files: |
| 50 | + rest_api_client.verify_files_are_present(files) |
| 51 | + else: |
| 52 | + files = list_source_files(args.extensions, ignored, not_ignored) |
| 53 | + if not files: |
| 54 | + logger.info("No source files need checking!") |
| 55 | + else: |
| 56 | + logger.info( |
| 57 | + "Giving attention to the following files:\n\t%s", |
| 58 | + "\n\t".join([f.name for f in files]), |
212 | 59 | )
|
213 |
| - return False |
214 |
| - return True |
| 60 | + end_log_group() |
| 61 | + |
| 62 | + (format_advice, tidy_advice) = capture_clang_tools_output( |
| 63 | + files, |
| 64 | + args.version, |
| 65 | + args.tidy_checks, |
| 66 | + args.style, |
| 67 | + args.lines_changed_only, |
| 68 | + args.database, |
| 69 | + args.extra_arg, |
| 70 | + ) |
215 | 71 |
|
| 72 | + start_log_group("Posting comment(s)") |
| 73 | + rest_api_client.post_feedback( |
| 74 | + files, |
| 75 | + format_advice, |
| 76 | + tidy_advice, |
| 77 | + args.thread_comments, |
| 78 | + args.no_lgtm, |
| 79 | + args.step_summary, |
| 80 | + args.file_annotations, |
| 81 | + args.style, |
| 82 | + ) |
| 83 | + end_log_group() |
216 | 84 |
|
217 |
| -def assemble_version_exec(tool_name: str, specified_version: str) -> Optional[str]: |
218 |
| - """Assembles the command to the executable of the given clang tool based on given |
219 |
| - version information. |
220 | 85 |
|
221 |
| - :param tool_name: The name of the clang tool to be executed. |
222 |
| - :param specified_version: The version number or the installed path to a version of |
223 |
| - the tool's executable. |
224 |
| - """ |
225 |
| - semver = specified_version.split(".") |
226 |
| - exe_path = None |
227 |
| - if semver and semver[0].isdigit(): # version info is not a path |
228 |
| - # let's assume the exe is in the PATH env var |
229 |
| - exe_path = shutil.which(f"{tool_name}-{specified_version}") |
230 |
| - elif specified_version: # treat value as a path to binary executable |
231 |
| - exe_path = shutil.which(tool_name, path=specified_version) |
232 |
| - if exe_path is not None: |
233 |
| - return exe_path |
234 |
| - return shutil.which(tool_name) |
| 86 | +if __name__ == "__main__": |
| 87 | + main() |
0 commit comments