Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 433 lines (337 sloc) 8.979 kB
affb3f0 @brixen Added DependencyGrapher.
brixen authored
1 # A simple processor to create a list of dependencies for a given source code
2 # file based on the #include files in the source file.
3 #
4 # The process involves three phases:
5 #
6 # 1. Parsing the source file for all C/C++ preprocessor control directives
7 # (#include, #ifdef, #ifndef, #else, #endif, #define, #undef, #if, #elif)
8 # 2. Executing the tree derived from step 1 in the context of a set of
9 # macro definitions.
10 # 3. Collecting the dependency list for each source file.
11 #
12 # In step 1, the tree resulting from parsing each included file is cached. If
13 # a later file of the same expanded relative path is included, the parse tree
14 # from the cache is returned.
15 #
16 # The system level macro definitions are retrieved using `cpp -dM`. This
17 # system set may be updated with new definitions or removed definitions while
18 # the parse tree is executed.
19
20 require 'set'
21
22 class DependencyGrapher
32cf036 @brixen Abstract /dev/null for Windows (luislavena).
brixen authored
23 DEV_NULL = RUBY_PLATFORM =~ /mingw|mswin/ ? 'NUL' : '/dev/null'
24
affb3f0 @brixen Added DependencyGrapher.
brixen authored
25 class ExpressionEvaluator
26 def initialize(expression)
27 @expression = expression
28 end
29
30 # TODO: when melbourne is running in MRI, use it to parse
31 # the expression and then evaluate it.
32 def evaluate(defines)
33 @expression == "0" ? false : true
34 end
35 end
36
37 class Node
38 def initialize(parser)
39 @parser = parser
40 end
41
42 def close
43 message = "unbalanced \#endif for #{@parser.stack_top.class} at line #{@parser.line}"
44 raise ParseError, message
45 end
46
47 def add_else
48 message = "invalid \#else for #{@parser.stack_top.class} at line #{@parser.line}"
49 end
50
51 # TODO: remove
52 def execute(defines, node)
53 puts "#execute not implemented for #{self.class}"
54 end
55 end
56
57 module Container
58 attr_accessor :body
59
60 def initialize(parser)
61 super parser
62 @body = []
63 end
64
65 def close
66 @parser.stack_pop
67 end
68
69 def execute_body(defines, node)
70 body.each { |x| x.execute(defines, node) }
71 end
72 end
73
74 module Conditional
75 def initialize(parser)
76 super parser
77 @else = nil
78 end
79
80 def add_else(node)
81 @else = node
82 end
83 end
84
85
86 class SourceFile < Node
87 include Container
88
89 attr_reader :name, :object_name, :includes, :dependencies
90
91 def initialize(name, parser)
92 super parser
93 @name = name
94 @object_name = name.sub(/((c(pp)?)|S)$/, 'o')
95 @includes = []
96 end
97
98 def execute(defines)
99 execute_body defines, self
100 end
101
102 def collect_dependencies
103 set = Set.new
104
105 set << @name
106 @includes.each { |x| x.collect_dependencies(set) }
107
108 @dependencies = set.to_a.sort
109 end
110
111 def print_dependencies(out, max)
112 str = "#{@object_name}:"
113 out.print str
114
115 width = str.size
116 @dependencies.each do |name|
117 size = name.size + 1
118 if width + size > max
119 width = 0
120 out.print " \\\n "
121 end
122
123 out.print " ", name
124 width += size
125 end
126
127 out.print "\n"
128 end
129 end
130
131 class IncludedFile < Node
132 include Container
133
134 attr_reader :name, :includes
135
136 def self.cache
137 @cache ||= {}
138 end
139
140 def initialize(name, parser)
141 super parser
142 @name = name
143 @includes = []
144 end
145
4e59bdd Report the file has a bad include
Evan Phoenix authored
146 def expand_filename(node)
affb3f0 @brixen Added DependencyGrapher.
brixen authored
147 return if File.exist? @name
148
149 @parser.directories.each do |dir|
150 path = File.join dir, @name
151 return @name = path if File.file? path
152 end
153
4e59bdd Report the file has a bad include
Evan Phoenix authored
154 raise Errno::ENOENT, "unable to find file to include: #{@name} from #{node.name}"
affb3f0 @brixen Added DependencyGrapher.
brixen authored
155 end
156
157 def execute(defines, node)
4e59bdd Report the file has a bad include
Evan Phoenix authored
158 expand_filename(node)
affb3f0 @brixen Added DependencyGrapher.
brixen authored
159
160 if cached = self.class.cache[@name.to_sym]
161 @body = cached.body
162 else
163 parser = FileParser.new self, @parser.directories
164 parser.parse_file @name
165 self.class.cache[@name.to_sym] = self
166 end
167
168 execute_body defines, self
169
170 node.includes << self
171 end
172
173 def collect_dependencies(set)
174 return if set.include? @name
175
176 set << @name
177 @includes.each { |x| x.collect_dependencies(set) }
178 end
179 end
180
181 class IfDefined < Node
182 include Conditional, Container
183
184 def initialize(macro, parser)
185 super parser
186 @macro = macro.strip
187 end
188
189 def execute(defines, node)
190 if defines.key? @macro
191 execute_body defines, node
192 elsif @else
193 @else.execute(defines, node)
194 end
195 end
196 end
197
198 class IfNotDefined < Node
199 include Conditional, Container
200
201 def initialize(macro, parser)
202 super parser
203 @macro = macro.strip
204 end
205
206 def execute(defines, node)
207 if !defines.key? @macro
208 execute_body defines, node
209 elsif @else
210 @else.execute(defines, node)
211 end
212 end
213 end
214
215 class If < Node
216 include Conditional, Container
217
218 def initialize(expression, parser)
219 super parser
220 @value = nil
221 @expression = expression.strip
222 end
223
224 def execute(defines, node)
225 @value = ExpressionEvaluator.new(@expression).evaluate defines
226
227 if @value
228 execute_body(defines, node)
229 elsif @else
230 @else.execute(defines, node)
231 end
232 end
233 end
234
235 class Else < Node
236 include Container
237
238 def execute(defines, node)
239 execute_body defines, node
240 end
241 end
242
243 class ElseIf < If; end
244
245 class Define < Node
246 def initialize(macro, parser)
247 super parser
248
249 macro.strip!
250 if index = macro.index(" ")
251 @name = macro[0..index-1]
252 @value = macro[index+1..-1]
253 @name, @value = macro.strip.split
254 else
255 @name = macro
256 @value = "1"
257 end
258 end
259
260 def execute(defines, node)
261 defines[@name] = @value
262 end
263 end
264
265 class Undefine < Node
266 def initialize(macro, parser)
267 super parser
268 @macro = macro.strip
269 end
270
271 def execute(defines, node)
272 defines.delete @macro
273 end
274 end
275
276 class ParseError < Exception; end
277
278 # Parses a file for all preprocessor control directives into a tree of Node
279 # objects. The parser can operate on a file or an array of lines.
280 class FileParser
281 attr_reader :line, :directories
282
283 def initialize(root, directories)
284 @stack = [root]
285 @directories = directories
286 end
287
288 # Parser operations
289
290 def add_body(node)
291 @stack.last.body << node
292 end
293
294 def add_else(node)
295 @stack.last.add_else node
296 end
297
298 def stack_push(node)
299 @stack.push node
300 end
301
302 def stack_pop
303 @stack.pop
304 end
305
306 def stack_top
307 @stack.last
308 end
309
310 def close
311 @stack.last.close
312 end
313
314 # Events
315
316 def process_include(name)
317 # We do not process any <files>. This could be enabled as
318 # an option, but we don't need it or want it now.
319 name =~ /\s*"([^"]+)".*$/
320 return unless $1
321
322 node = IncludedFile.new $1, self
323 add_body node
324 end
325
326 def process_ifdef(macro)
327 node = IfDefined.new macro, self
328 add_body node
329 stack_push node
330 end
331
332 def process_ifndef(macro)
333 node = IfNotDefined.new macro, self
334 add_body node
335 stack_push node
336 end
337
338 def process_endif(rest)
339 close
340 end
341
342 def process_else(rest)
343 node = Else.new self
344 add_else node
345 end
346
347 def process_define(macro)
348 node = Define.new macro, self
349 add_body node
350 end
351
352 def process_undef(macro)
353 node = Undefine.new macro, self
354 add_body node
355 end
356
357 def process_if(expression)
358 node = If.new expression, self
359 add_body node
360 stack_push node
361 end
362
363 def process_elif(expression)
364 node = ElseIf.new expression, self
365 add_else node
366 add_body node
367 stack_push node
368 end
369
370 # Parse methods
371
372 def parse_file(name)
373 parse IO.readlines(name)
374 end
375
376 def parse(lines)
377 @line = 0
378
379 lines.each do |line|
380 @line += 1
381 m = line.match(/^\s*#(include|ifdef|ifndef|endif|else|define|undef|if|elif)(.*)$/)
382
383 # TODO: continue reading if line ends in \
384
385 send :"process_#{m[1]}", m[2] if m
386 end
387 end
388 end
389
390 attr_accessor :file_names, :directories, :defines, :system_defines
391 attr_reader :sources
392
393 def initialize(files, directories=[], defines=nil)
394 @file_names = files
395 @directories = directories
396 @defines = defines
397
398 @system_defines = {}
399 @sources = []
400 end
401
402 def get_system_defines
32cf036 @brixen Abstract /dev/null for Windows (luislavena).
brixen authored
403 lines = `cpp -dM #{@defines} #{DEV_NULL}`.split("\n")
affb3f0 @brixen Added DependencyGrapher.
brixen authored
404
405 source = SourceFile.new "sytem_defines", self
406 parser = FileParser.new source, @directories
407 parser.parse lines
408
409 source.execute @system_defines
410 end
411
412 def process
413 get_system_defines
414
415 @file_names.each do |name|
416 source = SourceFile.new name, self
417 parser = FileParser.new source, @directories
418
419 parser.parse_file name
420 source.execute @system_defines.dup
421 source.collect_dependencies
422
423 @sources << source
424 end
425 end
426
427 def print_dependencies(out, max=72)
428 @sources.each do |source|
429 source.print_dependencies out, max
430 end
431 end
432 end
Something went wrong with that request. Please try again.