1"""Application."""
2import logging
3import os
4from typing import TYPE_CHECKING, Any, List, Type
5
6from ansiblelint import formatters
7from ansiblelint.color import console
8from ansiblelint.errors import MatchError
9
10if TYPE_CHECKING:
11    from argparse import Namespace
12
13
14_logger = logging.getLogger(__package__)
15
16
17class App:
18    """App class represents an execution of the linter."""
19
20    def __init__(self, options: "Namespace"):
21        """Construct app run based on already loaded configuration."""
22        options.skip_list = _sanitize_list_options(options.skip_list)
23        options.warn_list = _sanitize_list_options(options.warn_list)
24
25        self.options = options
26
27        formatter_factory = choose_formatter_factory(options)
28        self.formatter = formatter_factory(options.cwd, options.display_relative_path)
29
30    def render_matches(self, matches: List[MatchError]) -> None:
31        """Display given matches."""
32        if isinstance(self.formatter, formatters.CodeclimateJSONFormatter):
33            # If formatter CodeclimateJSONFormatter is chosen,
34            # then print only the matches in JSON
35            console.print(
36                self.formatter.format_result(matches), markup=False, highlight=False
37            )
38            return None
39
40        ignored_matches = [match for match in matches if match.ignored]
41        fatal_matches = [match for match in matches if not match.ignored]
42        # Displayed ignored matches first
43        if ignored_matches:
44            _logger.warning(
45                "Listing %s violation(s) marked as ignored, likely already known",
46                len(ignored_matches),
47            )
48            for match in ignored_matches:
49                if match.ignored:
50                    # highlight must be off or apostrophes may produce unexpected results
51                    console.print(self.formatter.format(match), highlight=False)
52        if fatal_matches:
53            _logger.warning(
54                "Listing %s violation(s) that are fatal", len(fatal_matches)
55            )
56            for match in fatal_matches:
57                if not match.ignored:
58                    console.print(self.formatter.format(match), highlight=False)
59
60        # If run under GitHub Actions we also want to emit output recognized by it.
61        if os.getenv('GITHUB_ACTIONS') == 'true' and os.getenv('GITHUB_WORKFLOW'):
62            formatter = formatters.AnnotationsFormatter(self.options.cwd, True)
63            for match in matches:
64                console.print(formatter.format(match), markup=False, highlight=False)
65
66
67def choose_formatter_factory(
68    options_list: "Namespace",
69) -> Type[formatters.BaseFormatter[Any]]:
70    """Select an output formatter based on the incoming command line arguments."""
71    r: Type[formatters.BaseFormatter[Any]] = formatters.Formatter
72    if options_list.format == 'quiet':
73        r = formatters.QuietFormatter
74    elif options_list.parseable_severity:
75        r = formatters.ParseableSeverityFormatter
76    elif options_list.format == 'codeclimate':
77        r = formatters.CodeclimateJSONFormatter
78    elif options_list.parseable or options_list.format == 'pep8':
79        r = formatters.ParseableFormatter
80    return r
81
82
83def _sanitize_list_options(tag_list: List[str]) -> List[str]:
84    """Normalize list options."""
85    # expand comma separated entries
86    tags = set()
87    for t in tag_list:
88        tags.update(str(t).split(','))
89    # remove duplicates, and return as sorted list
90    return sorted(set(tags))
91