summaryrefslogtreecommitdiffstats
path: root/ppapi/generators/idl_diff.py
blob: 0d15fe83e84702c01f36e645476715b19cade97e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
#!/usr/bin/env python
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import glob
import os
import subprocess
import sys

from idl_option import GetOption, Option, ParseOptions
from idl_outfile import IDLOutFile
#
# IDLDiff
#
# IDLDiff is a tool for comparing sets of IDL generated header files
# with the standard checked in headers.  It does this by capturing the
# output of the standard diff tool, parsing it into separate changes, then
# ignoring changes that are know to be safe, such as adding or removing
# blank lines, etc...
#

Option('gen', 'IDL generated files', default='hdir')
Option('src', 'Original ".h" files', default='../c')
Option('halt', 'Stop if a difference is found')
Option('diff', 'Directory holding acceptable diffs', default='diff')
Option('ok', 'Write out the diff file.')
# Change
#
# A Change object contains the previous lines, new news and change type.
#
class Change(object):
  def __init__(self, mode, was, now):
    self.mode = mode
    self.was = was
    self.now = now

  def Dump(self):
    if not self.was:
      print 'Adding %s' % self.mode
    elif not self.now:
      print 'Missing %s' % self.mode
    else:
      print 'Modifying %s' % self.mode

    for line in self.was:
      print 'src: >>%s<<' % line
    for line in self.now:
      print 'gen: >>%s<<' % line
    print

#
# IsCopyright
#
# Return True if this change is only a one line change in the copyright notice
# such as non-matching years.
#
def IsCopyright(change):
  if len(change.now) != 1 or len(change.was) != 1: return False
  if 'Copyright (c)' not in change.now[0]: return False
  if 'Copyright (c)' not in change.was[0]: return False
  return True

#
# IsBlankComment
#
# Return True if this change only removes a blank line from a comment
#
def IsBlankComment(change):
  if change.now: return False
  if len(change.was) != 1: return False
  if change.was[0].strip() != '*': return False
  return True

#
# IsBlank
#
# Return True if this change only adds or removes blank lines
#
def IsBlank(change):
  for line in change.now:
    if line: return False
  for line in change.was:
    if line: return False
  return True


#
# IsCppComment
#
# Return True if this change only going from C++ to C style
#
def IsToCppComment(change):
  if not len(change.now) or len(change.now) != len(change.was):
    return False
  for index in range(len(change.now)):
    was = change.was[index].strip()
    if was[:2] != '//':
      return False
    was = was[2:].strip()
    now = change.now[index].strip()
    if now[:2] != '/*':
      return False
    now = now[2:-2].strip()
    if now != was:
      return False
  return True


  return True

def IsMergeComment(change):
  if len(change.was) != 1: return False
  if change.was[0].strip() != '*': return False
  for line in change.now:
    stripped = line.strip()
    if stripped != '*' and stripped[:2] != '/*' and stripped[-2:] != '*/':
      return False
  return True
#
# IsSpacing
#
# Return True if this change is only different in the way 'words' are spaced
# such as in an enum:
#   ENUM_XXX   = 1,
#   ENUM_XYY_Y = 2,
# vs
#   ENUM_XXX = 1,
#   ENUM_XYY_Y = 2,
#
def IsSpacing(change):
  if len(change.now) != len(change.was): return False
  for i in range(len(change.now)):
    # Also ignore right side comments
    line = change.was[i]
    offs = line.find('//')
    if offs == -1:
      offs = line.find('/*')
    if offs >-1:
      line = line[:offs-1]

    words1 = change.now[i].split()
    words2 = line.split()
    if words1 != words2: return False
  return True

#
# IsInclude
#
# Return True if change has extra includes
#
def IsInclude(change):
  for line in change.was:
    if line.strip().find('struct'): return False
  for line in change.now:
    if line and '#include' not in line: return False
  return True

#
# IsCppComment
#
# Return True if the change is only missing C++ comments
#
def IsCppComment(change):
  if len(change.now): return False
  for line in change.was:
    line = line.strip()
    if line[:2] != '//': return False
  return True
#
# ValidChange
#
# Return True if none of the changes does not patch an above "bogus" change.
#
def ValidChange(change):
  if IsToCppComment(change): return False
  if IsCopyright(change): return False
  if IsBlankComment(change): return False
  if IsMergeComment(change): return False
  if IsBlank(change): return False
  if IsSpacing(change): return False
  if IsInclude(change): return False
  if IsCppComment(change): return False
  return True


#
# Swapped
#
# Check if the combination of last + next change signals they are both
# invalid such as swap of line around an invalid block.
#
def Swapped(last, next):
  if not last.now and not next.was and len(last.was) == len(next.now):
    cnt = len(last.was)
    for i in range(cnt):
      match = True
      for j in range(cnt):
        if last.was[j] != next.now[(i + j) % cnt]:
          match = False
          break;
      if match: return True
  if not last.was and not next.now and len(last.now) == len(next.was):
    cnt = len(last.now)
    for i in range(cnt):
      match = True
      for j in range(cnt):
        if last.now[i] != next.was[(i + j) % cnt]:
          match = False
          break;
      if match: return True
  return False


def FilterLinesIn(output):
  was = []
  now = []
  filter = []
  for index in range(len(output)):
    filter.append(False)
    line = output[index]
    if len(line) < 2: continue
    if line[0] == '<':
      if line[2:].strip() == '': continue
      was.append((index, line[2:]))
    elif line[0] == '>':
      if line[2:].strip() == '': continue
      now.append((index, line[2:]))
  for windex, wline in was:
    for nindex, nline in now:
      if filter[nindex]: continue
      if filter[windex]: continue
      if wline == nline:
        filter[nindex] = True
        filter[windex] = True
        if GetOption('verbose'):
          print "Found %d, %d >>%s<<" % (windex + 1, nindex + 1, wline)
  out = []
  for index in range(len(output)):
    if not filter[index]:
      out.append(output[index])

  return out
#
# GetChanges
#
# Parse the output into discrete change blocks.
#
def GetChanges(output):
  # Split on lines, adding an END marker to simply add logic
  lines = output.split('\n')
  lines = FilterLinesIn(lines)
  lines.append('END')

  changes = []
  was = []
  now = []
  mode = ''
  last = None

  for line in lines:
#    print "LINE=%s" % line
    if not line: continue

    elif line[0] == '<':
      if line[2:].strip() == '': continue
      # Ignore prototypes
      if len(line) > 10:
        words = line[2:].split()
        if len(words) == 2 and words[1][-1] == ';':
          if words[0] == 'struct' or words[0] == 'union':
            continue
      was.append(line[2:])
    elif line[0] == '>':
      if line[2:].strip() == '': continue
      if line[2:10] == '#include': continue
      now.append(line[2:])
    elif line[0] == '-':
      continue
    else:
      change = Change(line, was, now)
      was = []
      now = []
      if ValidChange(change):
          changes.append(change)
      if line == 'END':
        break

  return FilterChanges(changes)

def FilterChanges(changes):
  if len(changes) < 2: return changes
  out = []
  filter = [False for change in changes]
  for cur in range(len(changes)):
    for cmp in range(cur+1, len(changes)):
      if filter[cmp]:
        continue
      if Swapped(changes[cur], changes[cmp]):
        filter[cur] = True
        filter[cmp] = True
  for cur in range(len(changes)):
    if filter[cur]: continue
    out.append(changes[cur])
  return out

def Main(args):
  filenames = ParseOptions(args)
  if not filenames:
    gendir = os.path.join(GetOption('gen'), '*.h')
    filenames = sorted(glob.glob(gendir))
    srcdir = os.path.join(GetOption('src'), '*.h')
    srcs = sorted(glob.glob(srcdir))
    for name in srcs:
      name = os.path.split(name)[1]
      name = os.path.join(GetOption('gen'), name)
      if name not in filenames:
        print 'Missing: %s' % name

  for filename in filenames:
    gen = filename
    filename = filename[len(GetOption('gen')) + 1:]
    src = os.path.join(GetOption('src'), filename)
    diff = os.path.join(GetOption('diff'), filename)
    p = subprocess.Popen(['diff', src, gen], stdout=subprocess.PIPE)
    output, errors = p.communicate()

    try:
      input = open(diff, 'rt').read()
    except:
      input = ''

    if input != output:
      changes = GetChanges(output)
    else:
      changes = []

    if changes:
      print "\n\nDelta between:\n  src=%s\n  gen=%s\n" % (src, gen)
      for change in changes:
        change.Dump()
      print 'Done with %s\n\n' % src
      if GetOption('ok'):
        open(diff, 'wt').write(output)
      if GetOption('halt'):
        return 1
    else:
      print "\nSAME:\n  src=%s\n  gen=%s" % (src, gen)
      if input: print '  ** Matched expected diff. **'
      print '\n'


if __name__ == '__main__':
  sys.exit(Main(sys.argv[1:]))