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