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