xref: /openbsd-src/gnu/llvm/clang/utils/analyzer/ProjectMap.py (revision 24bb5fcea3ed904bc467217bdaadb5dfc618d5bf)
1import json
2import os
3
4from enum import Enum
5from typing import Any, Dict, List, NamedTuple, Optional, Tuple
6
7
8JSON = Dict[str, Any]
9
10
11DEFAULT_MAP_FILE = "projects.json"
12
13
14class DownloadType(str, Enum):
15    GIT = "git"
16    ZIP = "zip"
17    SCRIPT = "script"
18
19
20class ProjectInfo(NamedTuple):
21    """
22    Information about a project to analyze.
23    """
24    name: str
25    mode: int
26    source: DownloadType = DownloadType.SCRIPT
27    origin: str = ""
28    commit: str = ""
29    enabled: bool = True
30
31    def with_fields(self, **kwargs) -> "ProjectInfo":
32        """
33        Create a copy of this project info with customized fields.
34        NamedTuple is immutable and this is a way to create modified copies.
35
36          info.enabled = True
37          info.mode = 1
38
39        can be done as follows:
40
41          modified = info.with_fields(enbled=True, mode=1)
42        """
43        return ProjectInfo(**{**self._asdict(), **kwargs})
44
45
46class ProjectMap:
47    """
48    Project map stores info about all the "registered" projects.
49    """
50    def __init__(self, path: Optional[str] = None, should_exist: bool = True):
51        """
52        :param path: optional path to a project JSON file, when None defaults
53                     to DEFAULT_MAP_FILE.
54        :param should_exist: flag to tell if it's an exceptional situation when
55                             the project file doesn't exist, creates an empty
56                             project list instead if we are not expecting it to
57                             exist.
58        """
59        if path is None:
60            path = os.path.join(os.path.abspath(os.curdir), DEFAULT_MAP_FILE)
61
62        if not os.path.exists(path):
63            if should_exist:
64                raise ValueError(
65                    f"Cannot find the project map file {path}"
66                    f"\nRunning script for the wrong directory?\n")
67            else:
68                self._create_empty(path)
69
70        self.path = path
71        self._load_projects()
72
73    def save(self):
74        """
75        Save project map back to its original file.
76        """
77        self._save(self.projects, self.path)
78
79    def _load_projects(self):
80        with open(self.path) as raw_data:
81            raw_projects = json.load(raw_data)
82
83            if not isinstance(raw_projects, list):
84                raise ValueError(
85                    "Project map should be a list of JSON objects")
86
87            self.projects = self._parse(raw_projects)
88
89    @staticmethod
90    def _parse(raw_projects: List[JSON]) -> List[ProjectInfo]:
91        return [ProjectMap._parse_project(raw_project)
92                for raw_project in raw_projects]
93
94    @staticmethod
95    def _parse_project(raw_project: JSON) -> ProjectInfo:
96        try:
97            name: str = raw_project["name"]
98            build_mode: int = raw_project["mode"]
99            enabled: bool = raw_project.get("enabled", True)
100            source: DownloadType = raw_project.get("source", "zip")
101
102            if source == DownloadType.GIT:
103                origin, commit = ProjectMap._get_git_params(raw_project)
104            else:
105                origin, commit = "", ""
106
107            return ProjectInfo(name, build_mode, source, origin, commit,
108                               enabled)
109
110        except KeyError as e:
111            raise ValueError(
112                f"Project info is required to have a '{e.args[0]}' field")
113
114    @staticmethod
115    def _get_git_params(raw_project: JSON) -> Tuple[str, str]:
116        try:
117            return raw_project["origin"], raw_project["commit"]
118        except KeyError as e:
119            raise ValueError(
120                f"Profect info is required to have a '{e.args[0]}' field "
121                f"if it has a 'git' source")
122
123    @staticmethod
124    def _create_empty(path: str):
125        ProjectMap._save([], path)
126
127    @staticmethod
128    def _save(projects: List[ProjectInfo], path: str):
129        with open(path, "w") as output:
130            json.dump(ProjectMap._convert_infos_to_dicts(projects),
131                      output, indent=2)
132
133    @staticmethod
134    def _convert_infos_to_dicts(projects: List[ProjectInfo]) -> List[JSON]:
135        return [ProjectMap._convert_info_to_dict(project)
136                for project in projects]
137
138    @staticmethod
139    def _convert_info_to_dict(project: ProjectInfo) -> JSON:
140        whole_dict = project._asdict()
141        defaults = project._field_defaults
142
143        # there is no need in serializing fields with default values
144        for field, default_value in defaults.items():
145            if whole_dict[field] == default_value:
146                del whole_dict[field]
147
148        return whole_dict
149