Skip to content
Newer
Older
100644 437 lines (340 sloc) 8.94 KB
affb3f0 Added DependencyGrapher.
Brian Ford 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 Abstract /dev/null for Windows (luislavena).
Brian Ford authored
23 DEV_NULL = RUBY_PLATFORM =~ /mingw|mswin/ ? 'NUL' : '/dev/null'
24
affb3f0 Added DependencyGrapher.
Brian Ford 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|
9527512 Emit paths without drive letters in dependency grapher.
Brian Ford authored
117 # Fix for Windows drive letters. This string may
118 # be frozen so we do not modify it inline.
119 name = "/#{name[0, 1]}#{name[2..-1]}" if name[1] == ?:
120
affb3f0 Added DependencyGrapher.
Brian Ford authored
121 size = name.size + 1
122 if width + size > max
123 width = 0
124 out.print " \\\n "
125 end
126
127 out.print " ", name
128 width += size
129 end
130
131 out.print "\n"
132 end
133 end
134
135 class IncludedFile < Node
136 include Container
137
138 attr_reader :name, :includes
139
140 def self.cache
141 @cache ||= {}
142 end
143
144 def initialize(name, parser)
145 super parser
146 @name = name
147 @includes = []
148 end
149
4e59bdd Report the file has a bad include
Evan Phoenix authored
150 def expand_filename(node)
affb3f0 Added DependencyGrapher.
Brian Ford authored
151 return if File.exist? @name
152
153 @parser.directories.each do |dir|
154 path = File.join dir, @name
155 return @name = path if File.file? path
156 end
157
4e59bdd Report the file has a bad include
Evan Phoenix authored
158 raise Errno::ENOENT, "unable to find file to include: #{@name} from #{node.name}"
affb3f0 Added DependencyGrapher.
Brian Ford authored
159 end
160
161 def execute(defines, node)
4e59bdd Report the file has a bad include
Evan Phoenix authored
162 expand_filename(node)
affb3f0 Added DependencyGrapher.
Brian Ford authored
163
164 if cached = self.class.cache[@name.to_sym]
165 @body = cached.body
166 else
167 parser = FileParser.new self, @parser.directories
168 parser.parse_file @name
169 self.class.cache[@name.to_sym] = self
170 end
171
172 execute_body defines, self
173
174 node.includes << self
175 end
176
177 def collect_dependencies(set)
178 return if set.include? @name
179
180 set << @name
181 @includes.each { |x| x.collect_dependencies(set) }
182 end
183 end
184
185 class IfDefined < Node
186 include Conditional, Container
187
188 def initialize(macro, parser)
189 super parser
190 @macro = macro.strip
191 end
192
193 def execute(defines, node)
194 if defines.key? @macro
195 execute_body defines, node
196 elsif @else
197 @else.execute(defines, node)
198 end
199 end
200 end
201
202 class IfNotDefined < Node
203 include Conditional, Container
204
205 def initialize(macro, parser)
206 super parser
207 @macro = macro.strip
208 end
209
210 def execute(defines, node)
211 if !defines.key? @macro
212 execute_body defines, node
213 elsif @else
214 @else.execute(defines, node)
215 end
216 end
217 end
218
219 class If < Node
220 include Conditional, Container
221
222 def initialize(expression, parser)
223 super parser
224 @value = nil
225 @expression = expression.strip
226 end
227
228 def execute(defines, node)
229 @value = ExpressionEvaluator.new(@expression).evaluate defines
230
231 if @value
232 execute_body(defines, node)
233 elsif @else
234 @else.execute(defines, node)
235 end
236 end
237 end
238
239 class Else < Node
240 include Container
241
242 def execute(defines, node)
243 execute_body defines, node
244 end
245 end
246
247 class ElseIf < If; end
248
249 class Define < Node
250 def initialize(macro, parser)
251 super parser
252
253 macro.strip!
254 if index = macro.index(" ")
255 @name = macro[0..index-1]
256 @value = macro[index+1..-1]
257 @name, @value = macro.strip.split
258 else
259 @name = macro
260 @value = "1"
261 end
262 end
263
264 def execute(defines, node)
265 defines[@name] = @value
266 end
267 end
268
269 class Undefine < Node
270 def initialize(macro, parser)
271 super parser
272 @macro = macro.strip
273 end
274
275 def execute(defines, node)
276 defines.delete @macro
277 end
278 end
279
280 class ParseError < Exception; end
281
282 # Parses a file for all preprocessor control directives into a tree of Node
283 # objects. The parser can operate on a file or an array of lines.
284 class FileParser
285 attr_reader :line, :directories
286
287 def initialize(root, directories)
288 @stack = [root]
289 @directories = directories
290 end
291
292 # Parser operations
293
294 def add_body(node)
295 @stack.last.body << node
296 end
297
298 def add_else(node)
299 @stack.last.add_else node
300 end
301
302 def stack_push(node)
303 @stack.push node
304 end
305
306 def stack_pop
307 @stack.pop
308 end
309
310 def stack_top
311 @stack.last
312 end
313
314 def close
315 @stack.last.close
316 end
317
318 # Events
319
320 def process_include(name)
321 # We do not process any <files>. This could be enabled as
322 # an option, but we don't need it or want it now.
323 name =~ /\s*"([^"]+)".*$/
324 return unless $1
325
326 node = IncludedFile.new $1, self
327 add_body node
328 end
329
330 def process_ifdef(macro)
331 node = IfDefined.new macro, self
332 add_body node
333 stack_push node
334 end
335
336 def process_ifndef(macro)
337 node = IfNotDefined.new macro, self
338 add_body node
339 stack_push node
340 end
341
342 def process_endif(rest)
343 close
344 end
345
346 def process_else(rest)
347 node = Else.new self
348 add_else node
349 end
350
351 def process_define(macro)
352 node = Define.new macro, self
353 add_body node
354 end
355
356 def process_undef(macro)
357 node = Undefine.new macro, self
358 add_body node
359 end
360
361 def process_if(expression)
362 node = If.new expression, self
363 add_body node
364 stack_push node
365 end
366
367 def process_elif(expression)
368 node = ElseIf.new expression, self
369 add_else node
370 add_body node
371 stack_push node
372 end
373
374 # Parse methods
375
376 def parse_file(name)
377 parse IO.readlines(name)
378 end
379
380 def parse(lines)
381 @line = 0
382
383 lines.each do |line|
384 @line += 1
385 m = line.match(/^\s*#(include|ifdef|ifndef|endif|else|define|undef|if|elif)(.*)$/)
386
387 # TODO: continue reading if line ends in \
388
389 send :"process_#{m[1]}", m[2] if m
390 end
391 end
392 end
393
394 attr_accessor :file_names, :directories, :defines, :system_defines
395 attr_reader :sources
396
397 def initialize(files, directories=[], defines=nil)
398 @file_names = files
399 @directories = directories
400 @defines = defines
401
402 @system_defines = {}
403 @sources = []
404 end
405
406 def get_system_defines
32cf036 Abstract /dev/null for Windows (luislavena).
Brian Ford authored
407 lines = `cpp -dM #{@defines} #{DEV_NULL}`.split("\n")
affb3f0 Added DependencyGrapher.
Brian Ford authored
408
409 source = SourceFile.new "sytem_defines", self
410 parser = FileParser.new source, @directories
411 parser.parse lines
412
413 source.execute @system_defines
414 end
415
416 def process
417 get_system_defines
418
419 @file_names.each do |name|
420 source = SourceFile.new name, self
421 parser = FileParser.new source, @directories
422
423 parser.parse_file name
424 source.execute @system_defines.dup
425 source.collect_dependencies
426
427 @sources << source
428 end
429 end
430
431 def print_dependencies(out, max=72)
432 @sources.each do |source|
433 source.print_dependencies out, max
434 end
435 end
436 end
Something went wrong with that request. Please try again.