| 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 |