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