OLD | NEW |
| (Empty) |
1 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 | |
5 import re | |
6 | |
7 import util | |
8 | |
9 | |
10 CONTEXT_THRESHOLD = 12 | |
11 PROPERTY_NAME_RE = re.compile(r"^\s*'(\w+)'\s*:") | |
12 | |
13 | |
14 def nmin(*args): | |
15 """Calculates the minimum of |args|, ignoring None entries.""" | |
16 values = [v for v in args if v is not None] | |
17 return None if len(values) == 0 else min(values) | |
18 | |
19 | |
20 def nmax(*args): | |
21 """Calculates the maximum of |args|, ignoring None entries.""" | |
22 values = [v for v in args if v is not None] | |
23 return None if len(values) == 0 else max(values) | |
24 | |
25 | |
26 def nadd(a, b): | |
27 """Calculates a + b, returning None if either a or b is None""" | |
28 return None if (a is None or b is None) else a + b | |
29 | |
30 | |
31 def nsub(a, b): | |
32 """Calculates a - b, returning None if either a or b is None""" | |
33 return None if (a is None or b is None) else a - b | |
34 | |
35 | |
36 def get_indentation_level(line): | |
37 """Returns the indentation level (number of leading spaces) for |line|.""" | |
38 nspaces = len(line) - len(line.lstrip(' ')) | |
39 return None if nspaces == 0 else nspaces | |
40 | |
41 | |
42 class PolicyChangeParser(object): | |
43 """Parses a policy_templates.json diff to identify logical changes. | |
44 | |
45 This takes a list of triples of the form (old_line, new_line, text) as | |
46 returned by patching.ParsePatchToLines and produces a list of dictionaries | |
47 describing the logical changes that have been made. The dictionaries contain | |
48 these keys: | |
49 * start: A pair (old_line, new_line) indicating where the change starts. | |
50 * end: A pair (old_line, new_line) indicating where the change ends. | |
51 * comment_pos: A pair (old_line, new_line), indicating a suitable place to | |
52 put an inline comment. This is typically the line where the | |
53 policy name is found in the diff. | |
54 * additions: Whether there have been line additions. | |
55 * removals: Whether there have been line removals. | |
56 """ | |
57 | |
58 def __init__(self, lines): | |
59 self.lines = lines | |
60 self.chunks_list = [] | |
61 self.reset() | |
62 | |
63 def run(self): | |
64 """Main parsing function. | |
65 | |
66 The code goes over the diff line by line, keeping track of the current line. | |
67 It keeps track of the current line numbers, and where the last changes | |
68 happened in the old and new version of the file. | |
69 | |
70 Certain events trigger start of a new logical change. These are | |
71 discontinuities in the cursor position and decreases of the indentation | |
72 level. Once a block closes, the information for that block is recorded in | |
73 the result list. | |
74 """ | |
75 self.chunks_list = [] | |
76 self.last_change = [None, None] | |
77 cursor = [None, None] | |
78 self.reset() | |
79 for (a_line, b_line, line) in self.lines: | |
80 # Skip comment lines. | |
81 if line.startswith('#'): | |
82 continue | |
83 | |
84 # See whether the current line has a JSON property. | |
85 keyword = None | |
86 match = PROPERTY_NAME_RE.match(line) | |
87 if match: | |
88 keyword = match.group(1).lower() | |
89 | |
90 | |
91 # Check whether the current block closes. | |
92 line_indent = get_indentation_level(line) | |
93 if (self.block_indent is not None and | |
94 line_indent is not None and | |
95 line_indent < self.block_indent): | |
96 self.block_closed = True | |
97 | |
98 # Update various cursors. | |
99 cursor = [nmax(a_line, cursor[0]), nmax(b_line, cursor[1])] | |
100 offset = nmin(nsub(cursor[0], self.last_change[0]), | |
101 nsub(cursor[1], self.last_change[1])) | |
102 | |
103 # Update change tracking state. | |
104 if a_line is not None and b_line is None: | |
105 self.removals = True | |
106 self.last_change[0] = a_line | |
107 self.text_changed |= any([c.isalnum() for c in line]) | |
108 elif a_line is None and b_line is not None: | |
109 self.additions = True | |
110 self.last_change[1] = b_line | |
111 self.text_changed |= any([c.isalnum() for c in line]) | |
112 | |
113 # If the indentation block closes or the last chunk is too far away, | |
114 # assume a new one starts. | |
115 if (self.block_closed or | |
116 (offset is not None and (offset > CONTEXT_THRESHOLD))): | |
117 self.flush_chunk() | |
118 | |
119 # Try to figure out block indent from properties exclusively used for | |
120 # policy definitions. | |
121 if (self.block_indent is None and | |
122 keyword in ('id', 'schema', 'future', 'features', 'supported_on', | |
123 'example_value', 'deprecated')): | |
124 self.block_indent = line_indent | |
125 | |
126 # Put the comment on the policy name property if we see it fly by. | |
127 if keyword == 'name': | |
128 # Filter out name labels on enum items and schemas. | |
129 if self.block_indent is not None and self.block_indent != line_indent: | |
130 pass | |
131 elif a_line is not None and b_line is None: | |
132 self.comment_pos[0] = a_line | |
133 elif a_line is None and b_line is not None: | |
134 self.comment_pos[1] = b_line | |
135 | |
136 self.chunk_start = [nmin(self.last_change[0], self.chunk_start[0]), | |
137 nmin(self.last_change[1], self.chunk_start[1])] | |
138 | |
139 # Flush the last chunk. | |
140 if self.chunk_start != [None, None]: | |
141 self.flush_chunk() | |
142 | |
143 def flush_chunk(self): | |
144 if self.text_changed: | |
145 comment_pos = [nmax(self.chunk_start[0], self.comment_pos[0]), | |
146 nmax(self.chunk_start[1], self.comment_pos[1])] | |
147 self.chunks_list.append( | |
148 util.ObjectDict( | |
149 { 'start': self.chunk_start, | |
150 'end': [nadd(self.last_change[0], 1), | |
151 nadd(self.last_change[1], 1)], | |
152 'comment_pos': comment_pos, | |
153 'additions': self.additions, | |
154 'removals': self.removals })) | |
155 self.reset() | |
156 | |
157 def reset(self): | |
158 # This is called from __init__. | |
159 # pylint: disable=W0201 | |
160 self.chunk_start = [None, None] | |
161 self.last_change = [None, None] | |
162 self.comment_pos = [None, None] | |
163 self.block_indent = None | |
164 self.block_closed = False | |
165 self.additions = False | |
166 self.removals = False | |
167 self.text_changed = False | |
168 | |
169 | |
170 def parse(lines): | |
171 """Helper function to parse lines to a list of chunks directly.""" | |
172 parser = PolicyChangeParser(lines) | |
173 parser.run() | |
174 return parser.chunks_list | |
OLD | NEW |