xref: /netbsd-src/external/apache2/llvm/dist/clang/utils/analyzer/ProjectMap.py (revision e038c9c4676b0f19b1b7dd08a940c6ed64a6d5ae)
1*e038c9c4Sjoergimport json
2*e038c9c4Sjoergimport os
3*e038c9c4Sjoerg
4*e038c9c4Sjoergfrom enum import auto, Enum
5*e038c9c4Sjoergfrom typing import Any, Dict, List, NamedTuple, Optional, Tuple
6*e038c9c4Sjoerg
7*e038c9c4Sjoerg
8*e038c9c4SjoergJSON = Dict[str, Any]
9*e038c9c4Sjoerg
10*e038c9c4Sjoerg
11*e038c9c4SjoergDEFAULT_MAP_FILE = "projects.json"
12*e038c9c4Sjoerg
13*e038c9c4Sjoerg
14*e038c9c4Sjoergclass DownloadType(str, Enum):
15*e038c9c4Sjoerg    GIT = "git"
16*e038c9c4Sjoerg    ZIP = "zip"
17*e038c9c4Sjoerg    SCRIPT = "script"
18*e038c9c4Sjoerg
19*e038c9c4Sjoerg
20*e038c9c4Sjoergclass Size(int, Enum):
21*e038c9c4Sjoerg    """
22*e038c9c4Sjoerg    Size of the project.
23*e038c9c4Sjoerg
24*e038c9c4Sjoerg    Sizes do not directly correspond to the number of lines or files in the
25*e038c9c4Sjoerg    project.  The key factor that is important for the developers of the
26*e038c9c4Sjoerg    analyzer is the time it takes to analyze the project.  Here is how
27*e038c9c4Sjoerg    the following sizes map to times:
28*e038c9c4Sjoerg
29*e038c9c4Sjoerg    TINY:  <1min
30*e038c9c4Sjoerg    SMALL: 1min-10min
31*e038c9c4Sjoerg    BIG:   10min-1h
32*e038c9c4Sjoerg    HUGE:  >1h
33*e038c9c4Sjoerg
34*e038c9c4Sjoerg    The borders are a bit of a blur, especially because analysis time varies
35*e038c9c4Sjoerg    from one machine to another.  However, the relative times will stay pretty
36*e038c9c4Sjoerg    similar, and these groupings will still be helpful.
37*e038c9c4Sjoerg
38*e038c9c4Sjoerg    UNSPECIFIED is a very special case, which is intentionally last in the list
39*e038c9c4Sjoerg    of possible sizes.  If the user wants to filter projects by one of the
40*e038c9c4Sjoerg    possible sizes, we want projects with UNSPECIFIED size to be filtered out
41*e038c9c4Sjoerg    for any given size.
42*e038c9c4Sjoerg    """
43*e038c9c4Sjoerg    TINY = auto()
44*e038c9c4Sjoerg    SMALL = auto()
45*e038c9c4Sjoerg    BIG = auto()
46*e038c9c4Sjoerg    HUGE = auto()
47*e038c9c4Sjoerg    UNSPECIFIED = auto()
48*e038c9c4Sjoerg
49*e038c9c4Sjoerg    @staticmethod
50*e038c9c4Sjoerg    def from_str(raw_size: Optional[str]) -> "Size":
51*e038c9c4Sjoerg        """
52*e038c9c4Sjoerg        Construct a Size object from an optional string.
53*e038c9c4Sjoerg
54*e038c9c4Sjoerg        :param raw_size: optional string representation of the desired Size
55*e038c9c4Sjoerg                         object.  None will produce UNSPECIFIED size.
56*e038c9c4Sjoerg
57*e038c9c4Sjoerg        This method is case-insensitive, so raw sizes 'tiny', 'TINY', and
58*e038c9c4Sjoerg        'TiNy' will produce the same result.
59*e038c9c4Sjoerg        """
60*e038c9c4Sjoerg        if raw_size is None:
61*e038c9c4Sjoerg            return Size.UNSPECIFIED
62*e038c9c4Sjoerg
63*e038c9c4Sjoerg        raw_size_upper = raw_size.upper()
64*e038c9c4Sjoerg        # The implementation is decoupled from the actual values of the enum,
65*e038c9c4Sjoerg        # so we can easily add or modify it without bothering about this
66*e038c9c4Sjoerg        # function.
67*e038c9c4Sjoerg        for possible_size in Size:
68*e038c9c4Sjoerg            if possible_size.name == raw_size_upper:
69*e038c9c4Sjoerg                return possible_size
70*e038c9c4Sjoerg
71*e038c9c4Sjoerg        possible_sizes = [size.name.lower() for size in Size
72*e038c9c4Sjoerg                          # no need in showing our users this size
73*e038c9c4Sjoerg                          if size != Size.UNSPECIFIED]
74*e038c9c4Sjoerg        raise ValueError(f"Incorrect project size '{raw_size}'. "
75*e038c9c4Sjoerg                         f"Available sizes are {possible_sizes}")
76*e038c9c4Sjoerg
77*e038c9c4Sjoerg
78*e038c9c4Sjoergclass ProjectInfo(NamedTuple):
79*e038c9c4Sjoerg    """
80*e038c9c4Sjoerg    Information about a project to analyze.
81*e038c9c4Sjoerg    """
82*e038c9c4Sjoerg    name: str
83*e038c9c4Sjoerg    mode: int
84*e038c9c4Sjoerg    source: DownloadType = DownloadType.SCRIPT
85*e038c9c4Sjoerg    origin: str = ""
86*e038c9c4Sjoerg    commit: str = ""
87*e038c9c4Sjoerg    enabled: bool = True
88*e038c9c4Sjoerg    size: Size = Size.UNSPECIFIED
89*e038c9c4Sjoerg
90*e038c9c4Sjoerg    def with_fields(self, **kwargs) -> "ProjectInfo":
91*e038c9c4Sjoerg        """
92*e038c9c4Sjoerg        Create a copy of this project info with customized fields.
93*e038c9c4Sjoerg        NamedTuple is immutable and this is a way to create modified copies.
94*e038c9c4Sjoerg
95*e038c9c4Sjoerg          info.enabled = True
96*e038c9c4Sjoerg          info.mode = 1
97*e038c9c4Sjoerg
98*e038c9c4Sjoerg        can be done as follows:
99*e038c9c4Sjoerg
100*e038c9c4Sjoerg          modified = info.with_fields(enbled=True, mode=1)
101*e038c9c4Sjoerg        """
102*e038c9c4Sjoerg        return ProjectInfo(**{**self._asdict(), **kwargs})
103*e038c9c4Sjoerg
104*e038c9c4Sjoerg
105*e038c9c4Sjoergclass ProjectMap:
106*e038c9c4Sjoerg    """
107*e038c9c4Sjoerg    Project map stores info about all the "registered" projects.
108*e038c9c4Sjoerg    """
109*e038c9c4Sjoerg    def __init__(self, path: Optional[str] = None, should_exist: bool = True):
110*e038c9c4Sjoerg        """
111*e038c9c4Sjoerg        :param path: optional path to a project JSON file, when None defaults
112*e038c9c4Sjoerg                     to DEFAULT_MAP_FILE.
113*e038c9c4Sjoerg        :param should_exist: flag to tell if it's an exceptional situation when
114*e038c9c4Sjoerg                             the project file doesn't exist, creates an empty
115*e038c9c4Sjoerg                             project list instead if we are not expecting it to
116*e038c9c4Sjoerg                             exist.
117*e038c9c4Sjoerg        """
118*e038c9c4Sjoerg        if path is None:
119*e038c9c4Sjoerg            path = os.path.join(os.path.abspath(os.curdir), DEFAULT_MAP_FILE)
120*e038c9c4Sjoerg
121*e038c9c4Sjoerg        if not os.path.exists(path):
122*e038c9c4Sjoerg            if should_exist:
123*e038c9c4Sjoerg                raise ValueError(
124*e038c9c4Sjoerg                    f"Cannot find the project map file {path}"
125*e038c9c4Sjoerg                    f"\nRunning script for the wrong directory?\n")
126*e038c9c4Sjoerg            else:
127*e038c9c4Sjoerg                self._create_empty(path)
128*e038c9c4Sjoerg
129*e038c9c4Sjoerg        self.path = path
130*e038c9c4Sjoerg        self._load_projects()
131*e038c9c4Sjoerg
132*e038c9c4Sjoerg    def save(self):
133*e038c9c4Sjoerg        """
134*e038c9c4Sjoerg        Save project map back to its original file.
135*e038c9c4Sjoerg        """
136*e038c9c4Sjoerg        self._save(self.projects, self.path)
137*e038c9c4Sjoerg
138*e038c9c4Sjoerg    def _load_projects(self):
139*e038c9c4Sjoerg        with open(self.path) as raw_data:
140*e038c9c4Sjoerg            raw_projects = json.load(raw_data)
141*e038c9c4Sjoerg
142*e038c9c4Sjoerg            if not isinstance(raw_projects, list):
143*e038c9c4Sjoerg                raise ValueError(
144*e038c9c4Sjoerg                    "Project map should be a list of JSON objects")
145*e038c9c4Sjoerg
146*e038c9c4Sjoerg            self.projects = self._parse(raw_projects)
147*e038c9c4Sjoerg
148*e038c9c4Sjoerg    @staticmethod
149*e038c9c4Sjoerg    def _parse(raw_projects: List[JSON]) -> List[ProjectInfo]:
150*e038c9c4Sjoerg        return [ProjectMap._parse_project(raw_project)
151*e038c9c4Sjoerg                for raw_project in raw_projects]
152*e038c9c4Sjoerg
153*e038c9c4Sjoerg    @staticmethod
154*e038c9c4Sjoerg    def _parse_project(raw_project: JSON) -> ProjectInfo:
155*e038c9c4Sjoerg        try:
156*e038c9c4Sjoerg            name: str = raw_project["name"]
157*e038c9c4Sjoerg            build_mode: int = raw_project["mode"]
158*e038c9c4Sjoerg            enabled: bool = raw_project.get("enabled", True)
159*e038c9c4Sjoerg            source: DownloadType = raw_project.get("source", "zip")
160*e038c9c4Sjoerg            size = Size.from_str(raw_project.get("size", None))
161*e038c9c4Sjoerg
162*e038c9c4Sjoerg            if source == DownloadType.GIT:
163*e038c9c4Sjoerg                origin, commit = ProjectMap._get_git_params(raw_project)
164*e038c9c4Sjoerg            else:
165*e038c9c4Sjoerg                origin, commit = "", ""
166*e038c9c4Sjoerg
167*e038c9c4Sjoerg            return ProjectInfo(name, build_mode, source, origin, commit,
168*e038c9c4Sjoerg                               enabled, size)
169*e038c9c4Sjoerg
170*e038c9c4Sjoerg        except KeyError as e:
171*e038c9c4Sjoerg            raise ValueError(
172*e038c9c4Sjoerg                f"Project info is required to have a '{e.args[0]}' field")
173*e038c9c4Sjoerg
174*e038c9c4Sjoerg    @staticmethod
175*e038c9c4Sjoerg    def _get_git_params(raw_project: JSON) -> Tuple[str, str]:
176*e038c9c4Sjoerg        try:
177*e038c9c4Sjoerg            return raw_project["origin"], raw_project["commit"]
178*e038c9c4Sjoerg        except KeyError as e:
179*e038c9c4Sjoerg            raise ValueError(
180*e038c9c4Sjoerg                f"Profect info is required to have a '{e.args[0]}' field "
181*e038c9c4Sjoerg                f"if it has a 'git' source")
182*e038c9c4Sjoerg
183*e038c9c4Sjoerg    @staticmethod
184*e038c9c4Sjoerg    def _create_empty(path: str):
185*e038c9c4Sjoerg        ProjectMap._save([], path)
186*e038c9c4Sjoerg
187*e038c9c4Sjoerg    @staticmethod
188*e038c9c4Sjoerg    def _save(projects: List[ProjectInfo], path: str):
189*e038c9c4Sjoerg        with open(path, "w") as output:
190*e038c9c4Sjoerg            json.dump(ProjectMap._convert_infos_to_dicts(projects),
191*e038c9c4Sjoerg                      output, indent=2)
192*e038c9c4Sjoerg
193*e038c9c4Sjoerg    @staticmethod
194*e038c9c4Sjoerg    def _convert_infos_to_dicts(projects: List[ProjectInfo]) -> List[JSON]:
195*e038c9c4Sjoerg        return [ProjectMap._convert_info_to_dict(project)
196*e038c9c4Sjoerg                for project in projects]
197*e038c9c4Sjoerg
198*e038c9c4Sjoerg    @staticmethod
199*e038c9c4Sjoerg    def _convert_info_to_dict(project: ProjectInfo) -> JSON:
200*e038c9c4Sjoerg        whole_dict = project._asdict()
201*e038c9c4Sjoerg        defaults = project._field_defaults
202*e038c9c4Sjoerg
203*e038c9c4Sjoerg        # there is no need in serializing fields with default values
204*e038c9c4Sjoerg        for field, default_value in defaults.items():
205*e038c9c4Sjoerg            if whole_dict[field] == default_value:
206*e038c9c4Sjoerg                del whole_dict[field]
207*e038c9c4Sjoerg
208*e038c9c4Sjoerg        return whole_dict
209