idl_diff.py revision 5821806d5e7f356e8fa4b058a389a808ea183019
1#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import glob
7import os
8import subprocess
9import sys
10
11from idl_option import GetOption, Option, ParseOptions
12from idl_outfile import IDLOutFile
13#
14# IDLDiff
15#
16# IDLDiff is a tool for comparing sets of IDL generated header files
17# with the standard checked in headers.  It does this by capturing the
18# output of the standard diff tool, parsing it into separate changes, then
19# ignoring changes that are know to be safe, such as adding or removing
20# blank lines, etc...
21#
22
23Option('gen', 'IDL generated files', default='hdir')
24Option('src', 'Original ".h" files', default='../c')
25Option('halt', 'Stop if a difference is found')
26Option('diff', 'Directory holding acceptable diffs', default='diff')
27Option('ok', 'Write out the diff file.')
28# Change
29#
30# A Change object contains the previous lines, new news and change type.
31#
32class Change(object):
33  def __init__(self, mode, was, now):
34    self.mode = mode
35    self.was = was
36    self.now = now
37
38  def Dump(self):
39    if not self.was:
40      print 'Adding %s' % self.mode
41    elif not self.now:
42      print 'Missing %s' % self.mode
43    else:
44      print 'Modifying %s' % self.mode
45
46    for line in self.was:
47      print 'src: >>%s<<' % line
48    for line in self.now:
49      print 'gen: >>%s<<' % line
50    print
51
52#
53# IsCopyright
54#
55# Return True if this change is only a one line change in the copyright notice
56# such as non-matching years.
57#
58def IsCopyright(change):
59  if len(change.now) != 1 or len(change.was) != 1: return False
60  if 'Copyright (c)' not in change.now[0]: return False
61  if 'Copyright (c)' not in change.was[0]: return False
62  return True
63
64#
65# IsBlankComment
66#
67# Return True if this change only removes a blank line from a comment
68#
69def IsBlankComment(change):
70  if change.now: return False
71  if len(change.was) != 1: return False
72  if change.was[0].strip() != '*': return False
73  return True
74
75#
76# IsBlank
77#
78# Return True if this change only adds or removes blank lines
79#
80def IsBlank(change):
81  for line in change.now:
82    if line: return False
83  for line in change.was:
84    if line: return False
85  return True
86
87
88#
89# IsCppComment
90#
91# Return True if this change only going from C++ to C style
92#
93def IsToCppComment(change):
94  if not len(change.now) or len(change.now) != len(change.was):
95    return False
96  for index in range(len(change.now)):
97    was = change.was[index].strip()
98    if was[:2] != '//':
99      return False
100    was = was[2:].strip()
101    now = change.now[index].strip()
102    if now[:2] != '/*':
103      return False
104    now = now[2:-2].strip()
105    if now != was:
106      return False
107  return True
108
109
110  return True
111
112def IsMergeComment(change):
113  if len(change.was) != 1: return False
114  if change.was[0].strip() != '*': return False
115  for line in change.now:
116    stripped = line.strip()
117    if stripped != '*' and stripped[:2] != '/*' and stripped[-2:] != '*/':
118      return False
119  return True
120#
121# IsSpacing
122#
123# Return True if this change is only different in the way 'words' are spaced
124# such as in an enum:
125#   ENUM_XXX   = 1,
126#   ENUM_XYY_Y = 2,
127# vs
128#   ENUM_XXX = 1,
129#   ENUM_XYY_Y = 2,
130#
131def IsSpacing(change):
132  if len(change.now) != len(change.was): return False
133  for i in range(len(change.now)):
134    # Also ignore right side comments
135    line = change.was[i]
136    offs = line.find('//')
137    if offs == -1:
138      offs = line.find('/*')
139    if offs >-1:
140      line = line[:offs-1]
141
142    words1 = change.now[i].split()
143    words2 = line.split()
144    if words1 != words2: return False
145  return True
146
147#
148# IsInclude
149#
150# Return True if change has extra includes
151#
152def IsInclude(change):
153  for line in change.was:
154    if line.strip().find('struct'): return False
155  for line in change.now:
156    if line and '#include' not in line: return False
157  return True
158
159#
160# IsCppComment
161#
162# Return True if the change is only missing C++ comments
163#
164def IsCppComment(change):
165  if len(change.now): return False
166  for line in change.was:
167    line = line.strip()
168    if line[:2] != '//': return False
169  return True
170#
171# ValidChange
172#
173# Return True if none of the changes does not patch an above "bogus" change.
174#
175def ValidChange(change):
176  if IsToCppComment(change): return False
177  if IsCopyright(change): return False
178  if IsBlankComment(change): return False
179  if IsMergeComment(change): return False
180  if IsBlank(change): return False
181  if IsSpacing(change): return False
182  if IsInclude(change): return False
183  if IsCppComment(change): return False
184  return True
185
186
187#
188# Swapped
189#
190# Check if the combination of last + next change signals they are both
191# invalid such as swap of line around an invalid block.
192#
193def Swapped(last, next):
194  if not last.now and not next.was and len(last.was) == len(next.now):
195    cnt = len(last.was)
196    for i in range(cnt):
197      match = True
198      for j in range(cnt):
199        if last.was[j] != next.now[(i + j) % cnt]:
200          match = False
201          break;
202      if match: return True
203  if not last.was and not next.now and len(last.now) == len(next.was):
204    cnt = len(last.now)
205    for i in range(cnt):
206      match = True
207      for j in range(cnt):
208        if last.now[i] != next.was[(i + j) % cnt]:
209          match = False
210          break;
211      if match: return True
212  return False
213
214
215def FilterLinesIn(output):
216  was = []
217  now = []
218  filter = []
219  for index in range(len(output)):
220    filter.append(False)
221    line = output[index]
222    if len(line) < 2: continue
223    if line[0] == '<':
224      if line[2:].strip() == '': continue
225      was.append((index, line[2:]))
226    elif line[0] == '>':
227      if line[2:].strip() == '': continue
228      now.append((index, line[2:]))
229  for windex, wline in was:
230    for nindex, nline in now:
231      if filter[nindex]: continue
232      if filter[windex]: continue
233      if wline == nline:
234        filter[nindex] = True
235        filter[windex] = True
236        if GetOption('verbose'):
237          print "Found %d, %d >>%s<<" % (windex + 1, nindex + 1, wline)
238  out = []
239  for index in range(len(output)):
240    if not filter[index]:
241      out.append(output[index])
242
243  return out
244#
245# GetChanges
246#
247# Parse the output into discrete change blocks.
248#
249def GetChanges(output):
250  # Split on lines, adding an END marker to simply add logic
251  lines = output.split('\n')
252  lines = FilterLinesIn(lines)
253  lines.append('END')
254
255  changes = []
256  was = []
257  now = []
258  mode = ''
259  last = None
260
261  for line in lines:
262#    print "LINE=%s" % line
263    if not line: continue
264
265    elif line[0] == '<':
266      if line[2:].strip() == '': continue
267      # Ignore prototypes
268      if len(line) > 10:
269        words = line[2:].split()
270        if len(words) == 2 and words[1][-1] == ';':
271          if words[0] == 'struct' or words[0] == 'union':
272            continue
273      was.append(line[2:])
274    elif line[0] == '>':
275      if line[2:].strip() == '': continue
276      if line[2:10] == '#include': continue
277      now.append(line[2:])
278    elif line[0] == '-':
279      continue
280    else:
281      change = Change(line, was, now)
282      was = []
283      now = []
284      if ValidChange(change):
285          changes.append(change)
286      if line == 'END':
287        break
288
289  return FilterChanges(changes)
290
291def FilterChanges(changes):
292  if len(changes) < 2: return changes
293  out = []
294  filter = [False for change in changes]
295  for cur in range(len(changes)):
296    for cmp in range(cur+1, len(changes)):
297      if filter[cmp]:
298        continue
299      if Swapped(changes[cur], changes[cmp]):
300        filter[cur] = True
301        filter[cmp] = True
302  for cur in range(len(changes)):
303    if filter[cur]: continue
304    out.append(changes[cur])
305  return out
306
307def Main(args):
308  filenames = ParseOptions(args)
309  if not filenames:
310    gendir = os.path.join(GetOption('gen'), '*.h')
311    filenames = sorted(glob.glob(gendir))
312    srcdir = os.path.join(GetOption('src'), '*.h')
313    srcs = sorted(glob.glob(srcdir))
314    for name in srcs:
315      name = os.path.split(name)[1]
316      name = os.path.join(GetOption('gen'), name)
317      if name not in filenames:
318        print 'Missing: %s' % name
319
320  for filename in filenames:
321    gen = filename
322    filename = filename[len(GetOption('gen')) + 1:]
323    src = os.path.join(GetOption('src'), filename)
324    diff = os.path.join(GetOption('diff'), filename)
325    p = subprocess.Popen(['diff', src, gen], stdout=subprocess.PIPE)
326    output, errors = p.communicate()
327
328    try:
329      input = open(diff, 'rt').read()
330    except:
331      input = ''
332
333    if input != output:
334      changes = GetChanges(output)
335    else:
336      changes = []
337
338    if changes:
339      print "\n\nDelta between:\n  src=%s\n  gen=%s\n" % (src, gen)
340      for change in changes:
341        change.Dump()
342      print 'Done with %s\n\n' % src
343      if GetOption('ok'):
344        open(diff, 'wt').write(output)
345      if GetOption('halt'):
346        return 1
347    else:
348      print "\nSAME:\n  src=%s\n  gen=%s" % (src, gen)
349      if input: print '  ** Matched expected diff. **'
350      print '\n'
351
352
353if __name__ == '__main__':
354  sys.exit(Main(sys.argv[1:]))
355