Skip to content

Commit 86723c6

Browse files
committed
Be specific about what exceptions can be skipped
The Analyzer `#run` loop was rescuing *all* exceptions, which is excessive. I don't think we should hard-fail on all exceptions, either: the Ruby parser gem is definitely known to barf on some valid but esoteric Ruby code, and I wouldn't be surprised if the other parsers had similar edge cases. So this changes behavior to only skip files for a known set of catchable errors. The out-of-process parsers are a little tricky since all you can get from them is an exit code and output streams: for now wrapping those in our own exception class seems reasonable. I'm still catching all exceptions, but only so that we can log a message about which file is impacted before we re-raise. Since a raw exception we'll likely abort on may not contain helpful information about the file that triggered it, this would be helpful for debugging cases.
1 parent 90b5a4e commit 86723c6

File tree

8 files changed

+101
-23
lines changed

8 files changed

+101
-23
lines changed

lib/cc/engine/analyzers/analyzer_base.rb

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
1+
require "cc/engine/analyzers/parser_error"
2+
13
module CC
24
module Engine
35
module Analyzers
46
class Base
7+
RESCUABLE_ERRORS = [
8+
::CC::Engine::Analyzers::ParserError,
9+
::Racc::ParseError,
10+
::Timeout::Error,
11+
]
12+
513
def initialize(engine_config:)
614
@engine_config = engine_config
715
end
816

917
def run(file)
1018
process_file(file)
19+
rescue *RESCUABLE_ERRORS => ex
20+
$stderr.puts("Skipping file #{file} due to exception:")
21+
$stderr.puts("(#{ex.class}) #{ex.message} #{ex.backtrace.join("\n")}")
1122
rescue => ex
12-
$stderr.puts "Skipping file #{file} due to exception"
13-
$stderr.puts "(#{ex.class}) #{ex.message} #{ex.backtrace.join("\n")}"
23+
$stderr.puts("#{ex.class} error occurred processing file #{file}: aborting.")
24+
raise ex
1425
end
1526

1627
def files

lib/cc/engine/analyzers/javascript/parser.rb

+18-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
require 'timeout'
1+
require "open3"
2+
require "timeout"
3+
24

35
module CC
46
module Engine
@@ -55,14 +57,23 @@ def initialize(command, delegate)
5557

5658
def run(input, timeout = DEFAULT_TIMEOUT)
5759
Timeout.timeout(timeout) do
58-
IO.popen command, 'r+' do |io|
59-
io.puts input
60-
io.close_write
60+
Open3.popen3 command, "r+" do |stdin, stdout, stderr, wait_thr|
61+
stdin.puts input
62+
stdin.close
63+
64+
exit_code = wait_thr.value
65+
66+
output = stdout.gets
67+
stdout.close
6168

62-
output = io.gets
63-
io.close
69+
err_output = stderr.gets
70+
stderr.close
6471

65-
yield output if $?.to_i == 0
72+
if 0 == exit_code
73+
yield output
74+
else
75+
raise ::CC::Engine::Analyzers::ParserError, "Python parser exited with code #{exit_code}:\n#{err_output}"
76+
end
6677
end
6778
end
6879
end
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module CC
2+
module Engine
3+
module Analyzers
4+
ParserError = Class.new(StandardError)
5+
end
6+
end
7+
end

lib/cc/engine/analyzers/php/parser.rb

+15-6
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,23 @@ def initialize(command)
4848

4949
def run(input, timeout = DEFAULT_TIMEOUT)
5050
Timeout.timeout(timeout) do
51-
IO.popen command, 'r+' do |io|
52-
io.puts input
53-
io.close_write
51+
Open3.popen3 command, "r+" do |stdin, stdout, stderr, wait_thr|
52+
stdin.puts input
53+
stdin.close
5454

55-
output = io.gets
56-
io.close
55+
exit_code = wait_thr.value
5756

58-
yield output if $?.to_i == 0
57+
output = stdout.gets
58+
stdout.close
59+
60+
err_output = stderr.gets
61+
stderr.close
62+
63+
if 0 == exit_code
64+
yield output
65+
else
66+
raise ::CC::Engine::Analyzers::ParserError, "Python parser exited with code #{exit_code}:\n#{err_output}"
67+
end
5968
end
6069
end
6170
end

lib/cc/engine/analyzers/python/parser.rb

+18-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
require 'timeout'
2-
require 'json'
1+
require "timeout"
2+
require "json"
3+
require "open3"
34

45
module CC
56
module Engine
@@ -41,14 +42,23 @@ def initialize(command, delegate)
4142

4243
def run(input, timeout = DEFAULT_TIMEOUT)
4344
Timeout.timeout(timeout) do
44-
IO.popen command, "r+" do |io|
45-
io.puts input
46-
io.close_write
45+
Open3.popen3 command, "r+" do |stdin, stdout, stderr, wait_thr|
46+
stdin.puts input
47+
stdin.close
4748

48-
output = io.gets
49-
io.close
49+
exit_code = wait_thr.value
5050

51-
yield output if $?.to_i == 0
51+
output = stdout.gets
52+
stdout.close
53+
54+
err_output = stderr.gets
55+
stderr.close
56+
57+
if 0 == exit_code
58+
yield output
59+
else
60+
raise ::CC::Engine::Analyzers::ParserError, "Python parser exited with code #{exit_code}:\n#{err_output}"
61+
end
5262
end
5363
end
5464
end

spec/cc/engine/analyzers/javascript/main_spec.rb

+10
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@
3636
expect(json["content"]["body"]).to match /This issue has a mass of `99`/
3737
expect(json["fingerprint"]).to eq("55ae5d0990647ef496e9e0d315f9727d")
3838
end
39+
40+
it "skips unparsable files" do
41+
create_source_file("foo.js", <<-EOJS)
42+
function () { do(); // missing closing brace
43+
EOJS
44+
45+
expect {
46+
expect(run_engine(engine_conf)).to eq("")
47+
}.to output(/Skipping file/).to_stderr
48+
end
3949
end
4050

4151
it "does not flag duplicate comments" do

spec/cc/engine/analyzers/php/main_spec.rb

+10
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@
4848
expect(json["content"]["body"]).to match /This issue has a mass of `44`/
4949
expect(json["fingerprint"]).to eq("667da0e2bab866aa2fe9d014a65d57d9")
5050
end
51+
52+
it "skips unparsable files" do
53+
create_source_file("foo.php", <<-EOPHP)
54+
<?php blorb &; "fee
55+
EOPHP
56+
57+
expect {
58+
expect(run_engine(engine_conf)).to eq("")
59+
}.to output(/Skipping file/).to_stderr
60+
end
5161
end
5262

5363
def printed_issue

spec/cc/engine/analyzers/python/main_spec.rb

+10
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@
3636
expect(json["content"]["body"]).to match /This issue has a mass of `54`/
3737
expect(json["fingerprint"]).to eq("42b832387c997f54a2012efb2159aefc")
3838
end
39+
40+
it "skips unparsable files" do
41+
create_source_file("foo.py", <<-EOPY)
42+
---
43+
EOPY
44+
45+
expect {
46+
expect(run_engine(engine_conf)).to eq("")
47+
}.to output(/Skipping file/).to_stderr
48+
end
3949
end
4050

4151
def engine_conf

0 commit comments

Comments
 (0)