blob: f2176f5beea477bf7c70df7ddde5844bd3b9d3d1 [file] [log] [blame]
Paul Duffinb5cd5222022-02-28 19:06:49 +00001#!/usr/bin/env python
2#
3# Copyright (C) 2022 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16"""Verify that one set of hidden API flags is a subset of another."""
17
18from itertools import chain
19
20
21# pylint: disable=line-too-long
22class InteriorNode:
23 """An interior node in a trie.
24
25 Each interior node has a dict that maps from an element of a signature to
26 either another interior node or a leaf. Each interior node represents either
27 a package, class or nested class. Class members are represented by a Leaf.
28
29 Associating the set of flags [public-api] with the signature
30 "Ljava/lang/Object;->String()Ljava/lang/String;" will cause the following
31 nodes to be created:
32 Node()
33 ^- package:java -> Node()
34 ^- package:lang -> Node()
35 ^- class:Object -> Node()
36 ^- member:String()Ljava/lang/String; -> Leaf([public-api])
37
38 Associating the set of flags [blocked,core-platform-api] with the signature
39 "Ljava/lang/Character$UnicodeScript;->of(I)Ljava/lang/Character$UnicodeScript;"
40 will cause the following nodes to be created:
41 Node()
42 ^- package:java -> Node()
43 ^- package:lang -> Node()
44 ^- class:Character -> Node()
45 ^- class:UnicodeScript -> Node()
46 ^- member:of(I)Ljava/lang/Character$UnicodeScript;
47 -> Leaf([blocked,core-platform-api])
48
49 Attributes:
50 nodes: a dict from an element of the signature to the Node/Leaf
51 containing the next element/value.
52 """
53
54 # pylint: enable=line-too-long
55
56 def __init__(self):
57 self.nodes = {}
58
59 # pylint: disable=line-too-long
60 @staticmethod
61 def signature_to_elements(signature):
62 """Split a signature or a prefix into a number of elements:
63
64 1. The packages (excluding the leading L preceding the first package).
65 2. The class names, from outermost to innermost.
66 3. The member signature.
67 e.g.
68 Ljava/lang/Character$UnicodeScript;->of(I)Ljava/lang/Character$UnicodeScript;
69 will be broken down into these elements:
70 1. package:java
71 2. package:lang
72 3. class:Character
73 4. class:UnicodeScript
74 5. member:of(I)Ljava/lang/Character$UnicodeScript;
75 """
76 # Remove the leading L.
77 # - java/lang/Character$UnicodeScript;->of(I)Ljava/lang/Character$UnicodeScript;
78 text = signature.removeprefix("L")
79 # Split the signature between qualified class name and the class member
80 # signature.
81 # 0 - java/lang/Character$UnicodeScript
82 # 1 - of(I)Ljava/lang/Character$UnicodeScript;
83 parts = text.split(";->")
84 member = parts[1:]
85 # Split the qualified class name into packages, and class name.
86 # 0 - java
87 # 1 - lang
88 # 2 - Character$UnicodeScript
89 elements = parts[0].split("/")
90 packages = elements[0:-1]
91 class_name = elements[-1]
92 if class_name in ("*", "**"): # pylint: disable=no-else-return
93 # Cannot specify a wildcard and target a specific member
94 if len(member) != 0:
95 raise Exception(f"Invalid signature {signature}: contains "
96 f"wildcard {class_name} and "
97 f"member signature {member[0]}")
98 wildcard = [class_name]
99 # Assemble the parts into a single list, adding prefixes to identify
100 # the different parts.
101 # 0 - package:java
102 # 1 - package:lang
103 # 2 - *
104 return list(chain(["package:" + x for x in packages], wildcard))
105 else:
106 # Split the class name into outer / inner classes
107 # 0 - Character
108 # 1 - UnicodeScript
109 classes = class_name.split("$")
110 # Assemble the parts into a single list, adding prefixes to identify
111 # the different parts.
112 # 0 - package:java
113 # 1 - package:lang
114 # 2 - class:Character
115 # 3 - class:UnicodeScript
116 # 4 - member:of(I)Ljava/lang/Character$UnicodeScript;
117 return list(
118 chain(["package:" + x for x in packages],
119 ["class:" + x for x in classes],
120 ["member:" + x for x in member]))
121
122 # pylint: enable=line-too-long
123
124 def add(self, signature, value):
125 """Associate the value with the specific signature.
126
127 :param signature: the member signature
128 :param value: the value to associated with the signature
129 :return: n/a
130 """
131 # Split the signature into elements.
132 elements = self.signature_to_elements(signature)
133 # Find the Node associated with the deepest class.
134 node = self
135 for element in elements[:-1]:
136 if element in node.nodes:
137 node = node.nodes[element]
138 else:
139 next_node = InteriorNode()
140 node.nodes[element] = next_node
141 node = next_node
142 # Add a Leaf containing the value and associate it with the member
143 # signature within the class.
144 last_element = elements[-1]
145 if not last_element.startswith("member:"):
146 raise Exception(
147 f"Invalid signature: {signature}, does not identify a "
148 "specific member")
149 if last_element in node.nodes:
150 raise Exception(f"Duplicate signature: {signature}")
151 node.nodes[last_element] = Leaf(value)
152
153 def get_matching_rows(self, pattern):
154 """Get the values (plural) associated with the pattern.
155
156 e.g. If the pattern is a full signature then this will return a list
157 containing the value associated with that signature.
158
159 If the pattern is a class then this will return a list containing the
160 values associated with all members of that class.
161
162 If the pattern is a package then this will return a list containing the
163 values associated with all the members of all the classes in that
164 package and sub-packages.
165
166 If the pattern ends with "*" then the preceding part is treated as a
167 package and this will return a list containing the values associated
168 with all the members of all the classes in that package.
169
170 If the pattern ends with "**" then the preceding part is treated
171 as a package and this will return a list containing the values
172 associated with all the members of all the classes in that package and
173 all sub-packages.
174
175 :param pattern: the pattern which could be a complete signature or a
176 class, or package wildcard.
177 :return: an iterable containing all the values associated with the
178 pattern.
179 """
180 elements = self.signature_to_elements(pattern)
181 node = self
182
183 # Include all values from this node and all its children.
184 selector = lambda x: True
185
186 last_element = elements[-1]
187 if last_element in ("*", "**"):
188 elements = elements[:-1]
189 if last_element == "*":
190 # Do not include values from sub-packages.
191 selector = lambda x: not x.startswith("package:")
192
193 for element in elements:
194 if element in node.nodes:
195 node = node.nodes[element]
196 else:
197 return []
198 return chain.from_iterable(node.values(selector))
199
200 def values(self, selector):
201 """:param selector: a function that can be applied to a key in the nodes
202
203 attribute to determine whether to return its values.
204
205 :return: A list of iterables of all the values associated with
206 this node and its children.
207 """
208 values = []
209 self.append_values(values, selector)
210 return values
211
212 def append_values(self, values, selector):
213 """Append the values associated with this node and its children.
214
215 For each item (key, child) in nodes the child node's values are returned
216 if and only if the selector returns True when called on its key. A child
217 node's values are all the values associated with it and all its
218 descendant nodes.
219
220 :param selector: a function that can be applied to a key in the nodes
221 attribute to determine whether to return its values.
222 :param values: a list of a iterables of values.
223 """
224 for key, node in self.nodes.items():
225 if selector(key):
226 node.append_values(values, lambda x: True)
227
228
229class Leaf:
230 """A leaf of the trie
231
232 Attributes:
233 value: the value associated with this leaf.
234 """
235
236 def __init__(self, value):
237 self.value = value
238
239 def values(self, selector): # pylint: disable=unused-argument
240 """:return: A list of a list of the value associated with this node."""
241 return [[self.value]]
242
243 def append_values(self, values, selector): # pylint: disable=unused-argument
244 """Appends a list of the value associated with this node to the list.
245
246 :param values: a list of a iterables of values.
247 """
248 values.append([self.value])
249
250
251def signature_trie():
252 return InteriorNode()