summaryrefslogtreecommitdiff
path: root/.pytool/Plugin/UncrustifyCheck/UncrustifyCheck.py
blob: 9aeef5a5a3ed23bba8e0e804016cc4631123913b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
# @file UncrustifyCheck.py
#
# An edk2-pytool based plugin wrapper for Uncrustify
#
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: BSD-2-Clause-Patent
##
import configparser
import difflib
import errno
import logging
import os
import pathlib
import shutil
import timeit
from edk2toolext.environment import version_aggregator
from edk2toolext.environment.plugin_manager import PluginManager
from edk2toolext.environment.plugintypes.ci_build_plugin import ICiBuildPlugin
from edk2toolext.environment.plugintypes.uefi_helper_plugin import HelperFunctions
from edk2toolext.environment.var_dict import VarDict
from edk2toollib.gitignore_parser import parse_gitignore_lines
from edk2toollib.log.junit_report_format import JunitReportTestCase
from edk2toollib.uefi.edk2.path_utilities import Edk2Path
from edk2toollib.utility_functions import  RunCmd
from io import StringIO
from typing import Any, Dict, List, Tuple

#
# Provide more user friendly messages for certain scenarios
#
class UncrustifyException(Exception):
    def __init__(self, message, exit_code):
        super().__init__(message)
        self.exit_code = exit_code


class UncrustifyAppEnvVarNotFoundException(UncrustifyException):
    def __init__(self, message):
        super().__init__(message, -101)


class UncrustifyAppVersionErrorException(UncrustifyException):
    def __init__(self, message):
        super().__init__(message, -102)


class UncrustifyAppExecutionException(UncrustifyException):
    def __init__(self, message):
        super().__init__(message, -103)


class UncrustifyStalePluginFormattedFilesException(UncrustifyException):
    def __init__(self, message):
        super().__init__(message, -120)


class UncrustifyInputFileCreationErrorException(UncrustifyException):
    def __init__(self, message):
        super().__init__(message, -121)

class UncrustifyInvalidIgnoreStandardPathsException(UncrustifyException):
    def __init__(self, message):
        super().__init__(message, -122)

class UncrustifyGitIgnoreFileException(UncrustifyException):
    def __init__(self, message):
        super().__init__(message, -140)


class UncrustifyGitSubmoduleException(UncrustifyException):
    def __init__(self, message):
        super().__init__(message, -141)


class UncrustifyCheck(ICiBuildPlugin):
    """
    A CiBuildPlugin that uses Uncrustify to check the source files in the
    package being tested for coding standard issues.

    By default, the plugin runs against standard C source file extensions but
    its configuration can be modified through its configuration file.

    Configuration options:
    "UncrustifyCheck": {
        "AdditionalIncludePaths": [], # Additional paths to check formatting (wildcards supported).
        "AuditOnly": False,           # Don't fail the build if there are errors.  Just log them.
        "ConfigFilePath": "",         # Custom path to an Uncrustify config file.
        "IgnoreStandardPaths": [],    # Standard Plugin defined paths that should be ignored.
        "OutputFileDiffs": False,     # Output chunks of formatting diffs in the test case log.
                                      # This can significantly slow down the plugin on very large packages.
        "SkipGitExclusions": False    # Don't exclude git ignored files and files in git submodules.
    }
    """

    #
    # By default, use an "uncrustify.cfg" config file in the plugin directory
    # A package can override this path via "ConfigFilePath"
    #
    # Note: Values specified via "ConfigFilePath" are relative to the package
    #
    DEFAULT_CONFIG_FILE_PATH = os.path.join(
        pathlib.Path(__file__).parent.resolve(), "uncrustify.cfg")

    #
    # The extension used for formatted files produced by this plugin
    #
    FORMATTED_FILE_EXTENSION = ".uncrustify_plugin"

    #
    # A package can add any additional paths with "AdditionalIncludePaths"
    # A package can remove any of these paths with "IgnoreStandardPaths"
    #
    STANDARD_PLUGIN_DEFINED_PATHS = ("*.c", "*.h", "*.cpp")

    #
    # The Uncrustify application path should set in this environment variable
    #
    UNCRUSTIFY_PATH_ENV_KEY = "UNCRUSTIFY_CI_PATH"

    def GetTestName(self, packagename: str, environment: VarDict) -> Tuple:
        """ Provide the testcase name and classname for use in reporting

            Args:
              packagename: string containing name of package to build
              environment: The VarDict for the test to run in
            Returns:
                A tuple containing the testcase name and the classname
                (testcasename, classname)
                testclassname: a descriptive string for the testcase can include whitespace
                classname: should be patterned <packagename>.<plugin>.<optionally any unique condition>
        """
        return ("Check file coding standard compliance in " + packagename, packagename + ".UncrustifyCheck")

    def RunBuildPlugin(self, package_rel_path: str, edk2_path: Edk2Path, package_config: Dict[str, List[str]], environment_config: Any, plugin_manager: PluginManager, plugin_manager_helper: HelperFunctions, tc: JunitReportTestCase, output_stream=None) -> int:
        """
        External function of plugin. This function is used to perform the task of the CiBuild Plugin.

        Args:
          - package_rel_path: edk2 workspace relative path to the package
          - edk2_path: Edk2Path object with workspace and packages paths
          - package_config: Dictionary with the package configuration
          - environment_config: Environment configuration
          - plugin_manager: Plugin Manager Instance
          - plugin_manager_helper: Plugin Manager Helper Instance
          - tc: JUnit test case
          - output_stream: The StringIO output stream from this plugin (logging)

        Returns
          >0 : Number of errors found
          0  : Passed successfully
          -1 : Skipped for missing prereq
        """
        try:
            # Initialize plugin and check pre-requisites.
            self._initialize_environment_info(
                package_rel_path, edk2_path, package_config, tc)
            self._initialize_configuration()
            self._check_for_preexisting_formatted_files()

            # Log important context information.
            self._log_uncrustify_app_info()

            # Get template file contents if specified
            self._get_template_file_contents()

            # Create meta input files & directories
            self._create_temp_working_directory()
            self._create_uncrustify_file_list_file()

            self._run_uncrustify()

            # Post-execution actions.
            self._process_uncrustify_results()

        except UncrustifyException as e:
            self._tc.LogStdError(
                f"Uncrustify error {e.exit_code}. Details:\n\n{str(e)}")
            logging.warning(
                f"Uncrustify error {e.exit_code}. Details:\n\n{str(e)}")
            return -1
        else:
            if self._formatted_file_error_count > 0:
                if self._audit_only_mode:
                    logging.info(
                        "Setting test as skipped since AuditOnly is enabled")
                    self._tc.SetSkipped()
                    return -1
                else:
                    self._tc.SetFailed(
                        f"{self._plugin_name} failed due to {self._formatted_file_error_count} incorrectly formatted files.", "CHECK_FAILED")
            else:
                self._tc.SetSuccess()
            return self._formatted_file_error_count
        finally:
            self._cleanup_temporary_formatted_files()
            self._cleanup_temporary_directory()

    def _initialize_configuration(self) -> None:
        """
        Initializes plugin configuration.
        """
        self._initialize_app_info()
        self._initialize_config_file_info()
        self._initialize_file_to_format_info()
        self._initialize_test_case_output_options()

    def _check_for_preexisting_formatted_files(self) -> None:
        """
        Checks if any formatted files from prior execution are present.

        Existence of such files is an unexpected condition. This might result
        from an error that occurred during a previous run or a premature exit from a debug scenario. In any case, the package should be clean before starting a new run.
        """
        pre_existing_formatted_file_count = len(
            [str(path.resolve()) for path in pathlib.Path(self._abs_package_path).rglob(f'*{UncrustifyCheck.FORMATTED_FILE_EXTENSION}')])

        if pre_existing_formatted_file_count > 0:
            raise UncrustifyStalePluginFormattedFilesException(
                f"{pre_existing_formatted_file_count} formatted files already exist. To prevent overwriting these files, please remove them before running this plugin.")

    def _cleanup_temporary_directory(self) -> None:
        """
        Cleans up the temporary directory used for this execution instance.

        This removes the directory and all files created during this instance.
        """
        if hasattr(self, '_working_dir'):
            self._remove_tree(self._working_dir)

    def _cleanup_temporary_formatted_files(self) -> None:
        """
        Cleans up the temporary formmatted files produced by Uncrustify.

        This will recursively remove all formatted files generated by Uncrustify
        during this execution instance.
        """
        if hasattr(self, '_abs_package_path'):
            formatted_files = [str(path.resolve()) for path in pathlib.Path(
                self._abs_package_path).rglob(f'*{UncrustifyCheck.FORMATTED_FILE_EXTENSION}')]

            for formatted_file in formatted_files:
                os.remove(formatted_file)

    def _create_temp_working_directory(self) -> None:
        """
        Creates the temporary directory used for this execution instance.
        """
        self._working_dir = os.path.join(
            self._abs_workspace_path, "Build", ".pytool", "Plugin", f"{self._plugin_name}")

        try:
            pathlib.Path(self._working_dir).mkdir(parents=True, exist_ok=True)
        except OSError as e:
            raise UncrustifyInputFileCreationErrorException(
                f"Error creating plugin directory {self._working_dir}.\n\n{repr(e)}.")

    def _create_uncrustify_file_list_file(self) -> None:
        """
        Creates the file with the list of source files for Uncrustify to process.
        """
        self._app_input_file_path = os.path.join(
            self._working_dir, "uncrustify_file_list.txt")

        with open(self._app_input_file_path, 'w', encoding='utf8') as f:
            f.writelines(f"\n".join(self._abs_file_paths_to_format))

    def _execute_uncrustify(self) -> None:
        """
        Executes Uncrustify with the initialized configuration.
        """
        output = StringIO()
        self._app_exit_code = RunCmd(
            self._app_path,
            f"-c {self._app_config_file} -F {self._app_input_file_path} --if-changed --suffix {UncrustifyCheck.FORMATTED_FILE_EXTENSION}", outstream=output)
        self._app_output = output.getvalue().strip().splitlines()

    def _get_files_ignored_in_config(self):
        """"
        Returns a function that returns true if a given file string path is ignored in the plugin configuration file and false otherwise.
        """
        ignored_files = []
        if "IgnoreFiles" in self._package_config:
            ignored_files = self._package_config["IgnoreFiles"]

        # Pass "Package configuration file" as the source file path since
        # the actual configuration file name is unknown to this plugin and
        # this provides a generic description of the file that provided
        # the ignore file content.
        #
        # This information is only used for reporting (not used here) and
        # the ignore lines are being passed directly as they are given to
        # this plugin.
        return parse_gitignore_lines(ignored_files, "Package configuration file", self._abs_package_path)

    def _get_git_ignored_paths(self) -> List[str]:
        """"
        Returns a list of file absolute path strings to all files ignored in this git repository.

        If git is not found, an empty list will be returned.
        """
        if not shutil.which("git"):
            logging.warning(
                "Git is not found on this system. Git submodule paths will not be considered.")
            return []

        outstream_buffer = StringIO()
        exit_code = RunCmd("git", "ls-files --other",
                           workingdir=self._abs_workspace_path, outstream=outstream_buffer, logging_level=logging.NOTSET)
        if (exit_code != 0):
            raise UncrustifyGitIgnoreFileException(
                f"An error occurred reading git ignore settings. This will prevent Uncrustify from running against the expected set of files.")

        # Note: This will potentially be a large list, but at least sorted
        rel_paths = outstream_buffer.getvalue().strip().splitlines()
        abs_paths = []
        for path in rel_paths:
            abs_paths.append(
                os.path.normpath(os.path.join(self._abs_workspace_path, path)))
        return abs_paths

    def _get_git_submodule_paths(self) -> List[str]:
        """
        Returns a list of directory absolute path strings to the root of each submodule in the workspace repository.

        If git is not found, an empty list will be returned.
        """
        if not shutil.which("git"):
            logging.warning(
                "Git is not found on this system. Git submodule paths will not be considered.")
            return []

        if os.path.isfile(os.path.join(self._abs_workspace_path, ".gitmodules")):
            logging.info(
                f".gitmodules file found. Excluding submodules in {self._package_name}.")

            outstream_buffer = StringIO()
            exit_code = RunCmd("git", "config --file .gitmodules --get-regexp path", workingdir=self._abs_workspace_path, outstream=outstream_buffer, logging_level=logging.NOTSET)
            if (exit_code != 0):
                raise UncrustifyGitSubmoduleException(
                    f".gitmodule file detected but an error occurred reading the file. Cannot proceed with unknown submodule paths.")

            submodule_paths = []
            for line in outstream_buffer.getvalue().strip().splitlines():
                submodule_paths.append(
                    os.path.normpath(os.path.join(self._abs_workspace_path, line.split()[1])))

            return submodule_paths
        else:
            return []

    def _get_template_file_contents(self) -> None:
        """
        Gets the contents of Uncrustify template files if they are specified
        in the Uncrustify configuration file.
        """

        self._file_template_contents = None
        self._func_template_contents = None

        # Allow no value to allow "set" statements in the config file which do
        # not specify value assignment
        parser = configparser.ConfigParser(allow_no_value=True)
        with open(self._app_config_file, 'r') as cf:
            parser.read_string("[dummy_section]\n" + cf.read())

        try:
            file_template_name = parser["dummy_section"]["cmt_insert_file_header"]

            file_template_path = pathlib.Path(file_template_name)

            if not file_template_path.is_file():
                file_template_path = pathlib.Path(os.path.join(self._plugin_path, file_template_name))
                self._file_template_contents = file_template_path.read_text()
        except KeyError:
            logging.warning("A file header template is not specified in the config file.")
        except FileNotFoundError:
            logging.warning("The specified file header template file was not found.")
        try:
            func_template_name = parser["dummy_section"]["cmt_insert_func_header"]

            func_template_path = pathlib.Path(func_template_name)

            if not func_template_path.is_file():
                func_template_path = pathlib.Path(os.path.join(self._plugin_path, func_template_name))
                self._func_template_contents = func_template_path.read_text()
        except KeyError:
            logging.warning("A function header template is not specified in the config file.")
        except FileNotFoundError:
            logging.warning("The specified function header template file was not found.")

    def _initialize_app_info(self) -> None:
        """
        Initialize Uncrustify application information.

        This function will determine the application path and version.
        """
        # Verify Uncrustify is specified in the environment.
        if UncrustifyCheck.UNCRUSTIFY_PATH_ENV_KEY not in os.environ:
            raise UncrustifyAppEnvVarNotFoundException(
                f"Uncrustify environment variable {UncrustifyCheck.UNCRUSTIFY_PATH_ENV_KEY} is not present.")

        self._app_path = shutil.which('uncrustify', path=os.environ[UncrustifyCheck.UNCRUSTIFY_PATH_ENV_KEY])

        if self._app_path is None:
            raise FileNotFoundError(
                errno.ENOENT, os.strerror(errno.ENOENT), self._app_path)

        self._app_path = os.path.normcase(os.path.normpath(self._app_path))

        if not os.path.isfile(self._app_path):
            raise FileNotFoundError(
                errno.ENOENT, os.strerror(errno.ENOENT), self._app_path)

        # Verify Uncrustify is present at the expected path.
        return_buffer = StringIO()
        ret = RunCmd(self._app_path, "--version", outstream=return_buffer)
        if (ret != 0):
            raise UncrustifyAppVersionErrorException(
                f"Error occurred executing --version: {ret}.")

        # Log Uncrustify version information.
        self._app_version = return_buffer.getvalue().strip()
        self._tc.LogStdOut(f"Uncrustify version: {self._app_version}")
        version_aggregator.GetVersionAggregator().ReportVersion(
            "Uncrustify", self._app_version, version_aggregator.VersionTypes.INFO)

    def _initialize_config_file_info(self) -> None:
        """
        Initialize Uncrustify configuration file info.

        The config file path is relative to the package root.
        """
        self._app_config_file = UncrustifyCheck.DEFAULT_CONFIG_FILE_PATH
        if "ConfigFilePath" in self._package_config:
            self._app_config_file = self._package_config["ConfigFilePath"].strip()

            self._app_config_file = os.path.normpath(
                os.path.join(self._abs_package_path, self._app_config_file))

        if not os.path.isfile(self._app_config_file):
            raise FileNotFoundError(
                errno.ENOENT, os.strerror(errno.ENOENT), self._app_config_file)

    def _initialize_environment_info(self, package_rel_path: str, edk2_path: Edk2Path, package_config: Dict[str, List[str]], tc: JunitReportTestCase) -> None:
        """
        Initializes plugin environment information.
        """
        self._abs_package_path = edk2_path.GetAbsolutePathOnThisSystemFromEdk2RelativePath(
            package_rel_path)
        self._abs_workspace_path = edk2_path.WorkspacePath
        self._package_config = package_config
        self._package_name = os.path.basename(
            os.path.normpath(package_rel_path))
        self._plugin_name = self.__class__.__name__
        self._plugin_path = os.path.dirname(os.path.realpath(__file__))
        self._rel_package_path = package_rel_path
        self._tc = tc

    def _initialize_file_to_format_info(self) -> None:
        """
        Forms the list of source files for Uncrustify to process.
        """
        # Create a list of all the package relative file paths in the package to run against Uncrustify.
        rel_file_paths_to_format = list(
            UncrustifyCheck.STANDARD_PLUGIN_DEFINED_PATHS)

        # Allow the ci.yaml to remove any of the pre-defined standard paths
        if "IgnoreStandardPaths" in self._package_config:
            for a in self._package_config["IgnoreStandardPaths"]:
                if a.strip() in rel_file_paths_to_format:
                    self._tc.LogStdOut(
                        f"Ignoring standard path due to ci.yaml ignore: {a}")
                    rel_file_paths_to_format.remove(a.strip())
                else:
                    raise UncrustifyInvalidIgnoreStandardPathsException(f"Invalid IgnoreStandardPaths value: {a}")

        # Allow the ci.yaml to specify additional include paths for this package
        if "AdditionalIncludePaths" in self._package_config:
            rel_file_paths_to_format.extend(
                self._package_config["AdditionalIncludePaths"])

        self._abs_file_paths_to_format = []
        for path in rel_file_paths_to_format:
            self._abs_file_paths_to_format.extend(
                [str(path.resolve()) for path in pathlib.Path(self._abs_package_path).rglob(path)])

        # Remove files ignore in the plugin configuration file
        plugin_ignored_files = list(filter(self._get_files_ignored_in_config(), self._abs_file_paths_to_format))

        if plugin_ignored_files:
            logging.info(
                f"{self._package_name} file count before plugin ignore file exclusion: {len(self._abs_file_paths_to_format)}")
            for path in plugin_ignored_files:
                if path in self._abs_file_paths_to_format:
                    logging.info(f"  File ignored in plugin config file: {path}")
                    self._abs_file_paths_to_format.remove(path)
            logging.info(
                f"{self._package_name} file count after plugin ignore file exclusion: {len(self._abs_file_paths_to_format)}")

        if not "SkipGitExclusions" in self._package_config or not self._package_config["SkipGitExclusions"]:
            # Remove files ignored by git
            logging.info(
                f"{self._package_name} file count before git ignore file exclusion: {len(self._abs_file_paths_to_format)}")

            ignored_paths = self._get_git_ignored_paths()
            self._abs_file_paths_to_format = list(
                set(self._abs_file_paths_to_format).difference(ignored_paths))

            logging.info(
                f"{self._package_name} file count after git ignore file exclusion: {len(self._abs_file_paths_to_format)}")

            # Remove files in submodules
            logging.info(
                f"{self._package_name} file count before submodule exclusion: {len(self._abs_file_paths_to_format)}")

            submodule_paths = tuple(self._get_git_submodule_paths())
            for path in submodule_paths:
                logging.info(f"  submodule path: {path}")

            self._abs_file_paths_to_format = [
                f for f in self._abs_file_paths_to_format if not f.startswith(submodule_paths)]

            logging.info(
                f"{self._package_name} file count after submodule exclusion: {len(self._abs_file_paths_to_format)}")

        # Sort the files for more consistent results
        self._abs_file_paths_to_format.sort()

    def _initialize_test_case_output_options(self) -> None:
        """
        Initializes options that influence test case output.
        """
        self._audit_only_mode = False
        self._output_file_diffs = True

        if "AuditOnly" in self._package_config and self._package_config["AuditOnly"]:
            self._audit_only_mode = True

        if "OutputFileDiffs" in self._package_config and not self._package_config["OutputFileDiffs"]:
            self._output_file_diffs = False

    def _log_uncrustify_app_info(self) -> None:
        """
        Logs Uncrustify application information.
        """
        self._tc.LogStdOut(f"Found Uncrustify at {self._app_path}")
        self._tc.LogStdOut(f"Uncrustify version: {self._app_version}")
        self._tc.LogStdOut('\n')
        logging.info(f"Found Uncrustify at {self._app_path}")
        logging.info(f"Uncrustify version: {self._app_version}")
        logging.info('\n')

    def _process_uncrustify_results(self) -> None:
        """
        Process the results from Uncrustify.

        Determines whether formatting errors are present and logs failures.
        """
        formatted_files = [str(path.resolve()) for path in pathlib.Path(
            self._abs_package_path).rglob(f'*{UncrustifyCheck.FORMATTED_FILE_EXTENSION}')]

        self._formatted_file_error_count = len(formatted_files)

        if self._formatted_file_error_count > 0:
            logging.error(
                "Visit the following instructions to learn "
                "how to find the detailed formatting errors in Azure "
                "DevOps CI: "
                "https://github.com/tianocore/tianocore.github.io/wiki/EDK-II-Code-Formatting#how-to-find-uncrustify-formatting-errors-in-continuous-integration-ci")
            self._tc.LogStdError("Files with formatting errors:\n")

            if self._output_file_diffs:
                logging.info("Calculating file diffs. This might take a while...")

        for formatted_file in formatted_files:
            pre_formatted_file = formatted_file[:-
                                                len(UncrustifyCheck.FORMATTED_FILE_EXTENSION)]
            logging.error(pre_formatted_file)

            if (self._output_file_diffs or
                    self._file_template_contents is not None or
                    self._func_template_contents is not None):
                self._tc.LogStdError(
                    f"Formatting errors in {os.path.relpath(pre_formatted_file, self._abs_package_path)}\n")

                with open(formatted_file) as ff:
                    formatted_file_text = ff.read()

                    if (self._file_template_contents is not None and
                            self._file_template_contents in formatted_file_text):
                        self._tc.LogStdError(f"File header is missing in {os.path.relpath(pre_formatted_file, self._abs_package_path)}\n")

                    if (self._func_template_contents is not None and
                            self._func_template_contents in formatted_file_text):
                        self._tc.LogStdError(f"A function header is missing in {os.path.relpath(pre_formatted_file, self._abs_package_path)}\n")

                    if self._output_file_diffs:
                        with open(pre_formatted_file) as pf:
                            pre_formatted_file_text = pf.read()

                        for line in difflib.unified_diff(pre_formatted_file_text.split('\n'), formatted_file_text.split('\n'), fromfile=pre_formatted_file, tofile=formatted_file, n=3):
                            self._tc.LogStdError(line)

                        self._tc.LogStdError('\n')
            else:
                self._tc.LogStdError(pre_formatted_file)

    def _remove_tree(self, dir_path: str, ignore_errors: bool = False) -> None:
        """
        Helper for removing a directory. Over time there have been
        many private implementations of this due to reliability issues in the
        shutil implementations. To consolidate on a single function this helper is added.

        On error try to change file attributes. Also add retry logic.

        This function is temporarily borrowed from edk2toollib.utility_functions
        since the version used in edk2 is not recent enough to include the
        function.

        This function should be replaced by "RemoveTree" when it is available.

        Args:
          - dir_path: Path to directory to remove.
          - ignore_errors: Whether to ignore errors during removal
        """

        def _remove_readonly(func, path, _):
            """
            Private function to attempt to change permissions on file/folder being deleted.
            """
            os.chmod(path, os.stat.S_IWRITE)
            func(path)

        for _ in range(3):  # retry up to 3 times
            try:
                shutil.rmtree(dir_path, ignore_errors=ignore_errors, onerror=_remove_readonly)
            except OSError as err:
                logging.warning(f"Failed to fully remove {dir_path}: {err}")
            else:
                break
        else:
            raise RuntimeError(f"Failed to remove {dir_path}")

    def _run_uncrustify(self) -> None:
        """
        Runs Uncrustify for this instance of plugin execution.
        """
        logging.info("Executing Uncrustify. This might take a while...")
        start_time = timeit.default_timer()
        self._execute_uncrustify()
        end_time = timeit.default_timer() - start_time

        execution_summary = f"Uncrustify executed against {len(self._abs_file_paths_to_format)} files in {self._package_name} in {end_time:.2f} seconds.\n"

        self._tc.LogStdOut(execution_summary)
        logging.info(execution_summary)

        if self._app_exit_code != 0 and self._app_exit_code != 1:
            raise UncrustifyAppExecutionException(
                f"Error {str(self._app_exit_code)} returned from Uncrustify:\n\n{str(self._app_output)}")