Skip to content

Commit 6e5ca57

Browse files
authored
Add solargraph pre-commit hook for typechecking (#871)
[Solargraph](https://github.com/castwide/solargraph) supports typechecking Ruby files based on YARD tags - combined with overcommit, it can be used in an incremental way on large untyped codebases.
1 parent d55ffce commit 6e5ca57

File tree

3 files changed

+172
-0
lines changed

3 files changed

+172
-0
lines changed

config/default.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,20 @@ PreCommit:
799799
install_command: 'gem install slim_lint'
800800
include: '**/*.slim'
801801

802+
Solargraph:
803+
enabled: false
804+
description: 'Typecheck with Solargraph'
805+
requires_files: true
806+
required_executable: 'solargraph'
807+
install_command: 'gem install solargraph'
808+
flags: ['typecheck', '--level', 'strong']
809+
include: '**/*.rb'
810+
exclude:
811+
- 'spec/**/*.rb'
812+
- 'test/**/*.rb'
813+
- 'vendor/**/*.rb'
814+
- '.bundle/**/*.rb'
815+
802816
Sorbet:
803817
enabled: false
804818
description: 'Analyze with Sorbet'
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
require 'overcommit'
4+
require 'overcommit/hook/pre_commit/base'
5+
6+
module Overcommit
7+
module Hook
8+
module PreCommit
9+
# Runs `solargraph typecheck` against any modified Ruby files.
10+
#
11+
# @see https://github.com/castwide/solargraph
12+
class Solargraph < Base
13+
MESSAGE_REGEX = /^\s*(?<file>(?:\w:)?[^:]+):(?<line>\d+) - /.freeze
14+
15+
def run
16+
result = execute(command, args: applicable_files)
17+
return :pass if result.success?
18+
19+
stderr_lines = remove_harmless_glitches(result.stderr)
20+
violation_lines = result.stdout.split("\n").grep(MESSAGE_REGEX)
21+
if violation_lines.empty?
22+
if stderr_lines.empty?
23+
[:fail, 'Solargraph failed to run']
24+
else
25+
# let's feed it stderr so users see the errors
26+
extract_messages(stderr_lines, MESSAGE_REGEX)
27+
end
28+
else
29+
extract_messages(violation_lines, MESSAGE_REGEX)
30+
end
31+
end
32+
33+
private
34+
35+
# @param stderr [String]
36+
#
37+
# @return [Array<String>]
38+
def remove_harmless_glitches(stderr)
39+
stderr.split("\n").reject do |line|
40+
line.include?('[WARN]') ||
41+
line.include?('warning: parser/current is loading') ||
42+
line.include?('Please see https://github.com/whitequark')
43+
end
44+
end
45+
end
46+
end
47+
end
48+
end
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe Overcommit::Hook::PreCommit::Solargraph do
6+
let(:config) do
7+
Overcommit::ConfigurationLoader.default_configuration.merge(
8+
Overcommit::Configuration.new(
9+
'PreCommit' => {
10+
'Solargraph' => {
11+
'problem_on_unmodified_line' => problem_on_unmodified_line
12+
}
13+
}
14+
)
15+
)
16+
end
17+
let(:problem_on_unmodified_line) { 'ignore' }
18+
let(:context) { double('context') }
19+
let(:messages) { subject.run }
20+
let(:result) { double('result') }
21+
subject { described_class.new(config, context) }
22+
23+
before do
24+
subject.stub(:applicable_files).and_return(%w[file1.rb file2.rb])
25+
result.stub(:stderr).and_return(stderr)
26+
result.stub(:stdout).and_return(stdout)
27+
end
28+
29+
context 'when Solargraph exits successfully' do
30+
before do
31+
result.stub(:success?).and_return(true)
32+
subject.stub(:execute).and_return(result)
33+
end
34+
35+
context 'and it printed a message to stderr' do
36+
let(:stderr) { 'stderr unexpected message that must be fine since command successful' }
37+
let(:stdout) { '' }
38+
it { should pass }
39+
end
40+
41+
context 'and it printed a message to stdout' do
42+
let(:stderr) { '' }
43+
let(:stdout) { 'stdout message that must be fine since command successful' }
44+
it { should pass }
45+
end
46+
end
47+
48+
context 'when Solargraph exits unsucessfully' do
49+
before do
50+
result.stub(:success?).and_return(false)
51+
subject.stub(:execute).and_return(result)
52+
end
53+
54+
context 'and it reports typechecking issues' do
55+
let(:stdout) do
56+
normalize_indent(<<-MSG)
57+
/home/username/src/solargraph-rails/file1.rb:36 - Unresolved constant Solargraph::Parser::Legacy::NodeChainer
58+
/home/username/src/solargraph-rails/file2.rb:44 - Unresolved call to []
59+
/home/username/src/solargraph-rails/file2.rb:99 - Unresolved call to []
60+
Typecheck finished in 8.921023999806494 seconds.
61+
189 problems found in 14 of 16 files.
62+
MSG
63+
end
64+
65+
['', 'unexpected output'].each do |stderr_string|
66+
context "with stderr output of #{stderr_string.inspect}" do
67+
let(:stderr) { stderr_string }
68+
69+
it { should fail_hook }
70+
it 'reports only three errors and assumes stderr is harmless' do
71+
expect(messages.size).to eq 3
72+
end
73+
it 'parses filename' do
74+
expect(messages.first.file).to eq '/home/username/src/solargraph-rails/file1.rb'
75+
end
76+
it 'parses line number of messages' do
77+
expect(messages.first.line).to eq 36
78+
end
79+
it 'parses and returns error message content' do
80+
msg = '/home/username/src/solargraph-rails/file1.rb:36 - Unresolved constant Solargraph::Parser::Legacy::NodeChainer'
81+
expect(messages.first.content).to eq msg
82+
end
83+
end
84+
end
85+
end
86+
87+
context 'but it reports no typechecking issues' do
88+
let(:stdout) do
89+
normalize_indent(<<-MSG)
90+
Typecheck finished in 8.095239999704063 seconds.
91+
0 problems found in 0 of 16 files.
92+
MSG
93+
end
94+
95+
context 'with no stderr output' do
96+
let(:stderr) { '' }
97+
it 'should return no messages' do
98+
expect(messages).to eq([:fail, 'Solargraph failed to run'])
99+
end
100+
end
101+
102+
context 'with stderr output' do
103+
let(:stderr) { 'something' }
104+
it 'should raise' do
105+
expect { messages }.to raise_error(Overcommit::Exceptions::MessageProcessingError)
106+
end
107+
end
108+
end
109+
end
110+
end

0 commit comments

Comments
 (0)