Skip to content

Commit 8a14e13

Browse files
Andrew SzczepanskiAndrew Szczepanski
Andrew Szczepanski
authored and
Andrew Szczepanski
committed
Fix stack overflow for large projects
When large projects have a lot of missing objects in the first pass of parsing Ruby files we would run into stack overflows. This happens because, when there was a missing object in a parsed Ruby file, we would recursively call `#parse_remaining_files`. When this happens a lot the stack would get huge. We fix this by instead keeping a list of files that we want to retry and re-parse them in another pass. When we can no longer resolve any more files we break the loop.
1 parent 698b22c commit 8a14e13

File tree

4 files changed

+33
-42
lines changed

4 files changed

+33
-42
lines changed

lib/yard/handlers/base.rb

+3-8
Original file line numberDiff line numberDiff line change
@@ -562,14 +562,9 @@ def ensure_loaded!(object, max_retries = 1)
562562
return if object.root?
563563
return object unless object.is_a?(Proxy)
564564

565-
retries = 0
566-
while object.is_a?(Proxy)
567-
raise NamespaceMissingError, object if retries > max_retries
568-
log.debug "Missing object #{object} in file `#{parser.file}', moving it to the back of the line."
569-
parser.parse_remaining_files
570-
retries += 1
571-
end
572-
object
565+
log.debug "Missing object #{object} in file `#{parser.file}', moving it to the back of the line."
566+
globals.ordered_parser.files_to_retry << parser.file if globals.ordered_parser
567+
raise NamespaceMissingError, object
573568
end
574569

575570
# @group Macro Support

lib/yard/handlers/c/base.rb

+7-10
Original file line numberDiff line numberDiff line change
@@ -75,20 +75,17 @@ def namespace_for_variable(var)
7575
end
7676

7777
def ensure_variable_defined!(var, max_retries = 1)
78-
retries = 0
79-
object = nil
78+
object = namespace_for_variable(var)
79+
return object unless object.is_a?(Proxy)
8080

81-
loop do
82-
object = namespace_for_variable(var)
83-
break unless object.is_a?(Proxy)
81+
log.debug "Missing object #{object} in file `#{parser.file}', moving it to the back of the line."
8482

85-
raise NamespaceMissingError, object if retries > max_retries
86-
log.debug "Missing namespace variable #{var} in file `#{parser.file}', moving it to the back of the line."
87-
parser.parse_remaining_files
88-
retries += 1
83+
if globals.ordered_parser
84+
retryable_file = parser.file == "(stdin)" ? StringIO.new("void Init_Foo() { #{statement.source} }") : parser.file
85+
globals.ordered_parser.files_to_retry << retryable_file
8986
end
9087

91-
object
88+
raise NamespaceMissingError, object
9289
end
9390

9491
def namespaces

lib/yard/handlers/processor.rb

-13
Original file line numberDiff line numberDiff line change
@@ -131,19 +131,6 @@ def process(statements)
131131
end
132132
end
133133

134-
# Continue parsing the remainder of the files in the +globals.ordered_parser+
135-
# object. After the remainder of files are parsed, processing will continue
136-
# on the current file.
137-
#
138-
# @return [void]
139-
# @see Parser::OrderedParser
140-
def parse_remaining_files
141-
if globals.ordered_parser
142-
globals.ordered_parser.parse
143-
log.debug("Re-processing #{@file}...")
144-
end
145-
end
146-
147134
# Searches for all handlers in {Base.subclasses} that match the +statement+
148135
#
149136
# @param statement the statement object to match.

lib/yard/parser/source_parser.rb

+23-11
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,14 @@ class UndocumentableError < RuntimeError; end
1212
# Raised when the parser sees a Ruby syntax error
1313
class ParserSyntaxError < UndocumentableError; end
1414

15-
# Responsible for parsing a list of files in order. The
16-
# {#parse} method of this class can be called from the
17-
# {SourceParser#globals} globals state list to re-enter
18-
# parsing for the remainder of files in the list recursively.
19-
#
20-
# @see Processor#parse_remaining_files
15+
# Responsible for parsing a list of files in order.
2116
class OrderedParser
2217
# @return [Array<String>] the list of remaining files to parse
2318
attr_accessor :files
2419

20+
# @return [Array<String>] files to parse again after our first pass
21+
attr_accessor :files_to_retry
22+
2523
# Creates a new OrderedParser with the global state and a list
2624
# of files to parse.
2725
#
@@ -33,19 +31,33 @@ class OrderedParser
3331
def initialize(global_state, files)
3432
@global_state = global_state
3533
@files = files.dup
34+
@files_to_retry = []
3635
@global_state.ordered_parser = self
3736
end
3837

39-
# Parses the remainder of the {#files} list.
40-
#
41-
# @see Processor#parse_remaining_files
38+
# Parses the remainder of the {#files} list. Any files that had issues
39+
# parsing will be done in multiple passes until the size of the files
40+
# remaining does not change.
4241
def parse
43-
until files.empty?
44-
file = files.shift
42+
files.each do |file|
4543
log.capture("Parsing #{file}") do
4644
SourceParser.new(SourceParser.parser_type, @global_state).parse(file)
4745
end
4846
end
47+
48+
loop do
49+
prev_length = @files_to_retry.length
50+
files_to_parse_now = @files_to_retry.dup
51+
@files_to_retry = []
52+
53+
files_to_parse_now.each do |file|
54+
log.capture("Re-processing #{file}") do
55+
SourceParser.new(SourceParser.parser_type, @global_state).parse(file)
56+
end
57+
end
58+
59+
break if @files_to_retry.length >= prev_length
60+
end
4961
end
5062
end
5163

0 commit comments

Comments
 (0)