Skip to content

Commit 90bc4cf

Browse files
committed
(GH-28) Add mechanism for Puppetfile validation
Previously the language server could identify a Puppetfile file but would not actually do anything with it. This commit; * Adds a basic Puppetfile parser based on the skeleton of the R10K gem * Adds valiadation that the ruby style Puppetfile could be parsed and reports diagnostic errors if found * Integrates into the async validation process * Adds unit tests for the provider
1 parent 3fdc7bb commit 90bc4cf

File tree

6 files changed

+271
-11
lines changed

6 files changed

+271
-11
lines changed

lib/puppet-languageserver/providers.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
manifest/definition_provider
55
manifest/validation_provider
66
manifest/hover_provider
7+
puppetfile/r10k/puppetfile
8+
puppetfile/validation_provider
79
].each do |lib|
810
begin
911
require "puppet-languageserver/#{lib}"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
module PuppetLanguageServer
2+
module Puppetfile
3+
module R10K
4+
PUPPETFILE_MONIKER ||= 'Puppetfile'.freeze
5+
6+
class Puppetfile
7+
def load!(puppetfile_contents)
8+
puppetfile = DSL.new(self)
9+
puppetfile.instance_eval(puppetfile_contents, PUPPETFILE_MONIKER)
10+
end
11+
12+
class DSL
13+
def initialize(parent)
14+
@parent = parent
15+
end
16+
17+
# @param [String] name
18+
# @param [*Object] args
19+
def mod(_name, _args = nil)
20+
end
21+
22+
# @param [String] forge
23+
def forge(_location)
24+
end
25+
26+
# @param [String] moduledir
27+
def moduledir(_location)
28+
end
29+
30+
def method_missing(method, *_args) # rubocop:disable Style/MethodMissing
31+
raise NoMethodError, format("Unknown method '%<method>s'", method: method)
32+
end
33+
end
34+
end
35+
end
36+
end
37+
end
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
module PuppetLanguageServer
2+
module Puppetfile
3+
module ValidationProvider
4+
def self.max_line_length
5+
# TODO: ... need to figure out the actual line length
6+
1000
7+
end
8+
9+
def self.validate(content, _workspace, _max_problems = 100)
10+
result = []
11+
# TODO: Need to implement max_problems
12+
_problems = 0
13+
14+
# Attempt to parse the file
15+
puppetfile = nil
16+
begin
17+
puppetfile = PuppetLanguageServer::Puppetfile::R10K::Puppetfile.new
18+
puppetfile.load!(content)
19+
rescue StandardError, SyntaxError, LoadError => detail
20+
# Find the originating error from within the puppetfile
21+
loc = detail.backtrace_locations
22+
.select { |item| item.absolute_path == PuppetLanguageServer::Puppetfile::R10K::PUPPETFILE_MONIKER }
23+
.first
24+
start_line_number = loc.nil? ? 0 : loc.lineno - 1 # Line numbers from ruby are base 1
25+
end_line_number = loc.nil? ? content.lines.count - 1 : loc.lineno - 1 # Line numbers from ruby are base 1
26+
# Note - Ruby doesn't give a character position so just highlight the entire line
27+
result << LanguageServer::Diagnostic.create('severity' => LanguageServer::DIAGNOSTICSEVERITY_ERROR,
28+
'fromline' => start_line_number,
29+
'toline' => end_line_number,
30+
'fromchar' => 0,
31+
'tochar' => max_line_length,
32+
'source' => 'Puppet',
33+
'message' => detail.to_s)
34+
35+
puppetfile = nil
36+
end
37+
return result if puppetfile.nil?
38+
39+
result
40+
end
41+
end
42+
end
43+
end

lib/puppet-languageserver/validation_queue.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ module ValidationQueue
1414
def self.enqueue(file_uri, doc_version, workspace, connection_object)
1515
document_type = PuppetLanguageServer::DocumentStore.document_type(file_uri)
1616

17-
unless %i[manifest epp].include?(document_type)
17+
unless %i[manifest epp puppetfile].include?(document_type)
1818
# Can't validate these types so just emit an empty validation result
1919
connection_object.reply_diagnostics(file_uri, [])
2020
return
@@ -86,6 +86,8 @@ def self.validate(document_type, content, workspace)
8686
PuppetLanguageServer::Manifest::ValidationProvider.validate(content, workspace)
8787
when :epp
8888
PuppetLanguageServer::Epp::ValidationProvider.validate(content, workspace)
89+
when :puppetfile
90+
PuppetLanguageServer::Puppetfile::ValidationProvider.validate(content, workspace)
8991
else
9092
[]
9193
end
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
require 'spec_helper'
2+
3+
describe 'PuppetLanguageServer::Puppetfile::ValidationProvider' do
4+
let(:subject) { PuppetLanguageServer::Puppetfile::ValidationProvider }
5+
6+
describe "#validate" do
7+
context 'with an empty Puppetfile' do
8+
let(:content) { '' }
9+
it 'should return no validation errors' do
10+
result = subject.validate(content, nil)
11+
12+
expect(result).to eq([])
13+
end
14+
end
15+
16+
context 'with a valid Puppetfile' do
17+
let(:content) do <<-EOT
18+
forge 'https://forge.puppetlabs.com/'
19+
20+
# Modules from the Puppet Forge
21+
mod 'puppetlabs-somemodule', '1.0.0'
22+
23+
# Git style modules
24+
mod 'gitcommitmodule',
25+
:git => 'https://github.com/username/repo',
26+
:commit => 'abc123'
27+
mod 'gittagmodule',
28+
:git => 'https://github.com/username/repo',
29+
:tag => '0.1'
30+
EOT
31+
end
32+
33+
it 'should return no validation errors' do
34+
result = subject.validate(content, nil)
35+
36+
expect(result).to eq([])
37+
end
38+
end
39+
40+
context 'with a syntax error in the Puppetfile' do
41+
let(:content) do <<-EOT
42+
forge 'https://forge.puppetlabs.com/'
43+
44+
# Modules from the Puppet Forge
45+
mod 'puppetlabs-somemodule', '1.0.0'
46+
47+
# Git style modules
48+
mod 'gitcommitmodule',
49+
:git => 'https://github.com/username/repo',
50+
:commit => 'abc123'
51+
mod 'gittagmodule',
52+
:git => 'https://github.com/username/repo',
53+
:tag => '0.1'
54+
} # I am a sytnax error
55+
EOT
56+
end
57+
58+
it 'should return a validation error' do
59+
lint_error = subject.validate(content, nil)[0]
60+
61+
expect(lint_error['source']).to eq('Puppet')
62+
expect(lint_error['message']).to match('syntax error')
63+
expect(lint_error['range']).to_not be_nil
64+
expect(lint_error['severity']).to eq(LanguageServer::DIAGNOSTICSEVERITY_ERROR)
65+
end
66+
end
67+
68+
context 'with a loading error in the Puppetfile' do
69+
let(:content) do <<-EOT
70+
forge 'https://forge.puppetlabs.com/'
71+
72+
# Modules from the Puppet Forge
73+
mod 'puppetlabs-somemodule', '1.0.0'
74+
75+
require 'not_loadable' # I am a load error
76+
77+
# Git style modules
78+
mod 'gitcommitmodule',
79+
:git => 'https://github.com/username/repo',
80+
:commit => 'abc123'
81+
mod 'gittagmodule',
82+
:git => 'https://github.com/username/repo',
83+
:tag => '0.1'
84+
EOT
85+
end
86+
87+
it 'should return a validation error' do
88+
lint_error = subject.validate(content, nil)[0]
89+
90+
expect(lint_error['source']).to eq('Puppet')
91+
expect(lint_error['message']).to match('not_loadable')
92+
expect(lint_error['range']['start']['line']).to eq(5)
93+
expect(lint_error['range']['end']['line']).to eq(5)
94+
expect(lint_error['range']).to_not be_nil
95+
expect(lint_error['severity']).to eq(LanguageServer::DIAGNOSTICSEVERITY_ERROR)
96+
end
97+
end
98+
99+
context 'with a standard error in the Puppetfile' do
100+
let(:content) do <<-EOT
101+
forge 'https://forge.puppetlabs.com/'
102+
103+
# Modules from the Puppet Forge
104+
mod 'puppetlabs-somemodule', '1.0.0'
105+
106+
# Git style modules
107+
mod 'gitcommitmodule',
108+
:git => 'https://github.com/username/repo',
109+
:commit => 'abc123'
110+
111+
raise 'A Mock Runtime Error'
112+
113+
mod 'gittagmodule',
114+
:git => 'https://github.com/username/repo',
115+
:tag => '0.1'
116+
EOT
117+
end
118+
119+
it 'should return a validation error' do
120+
lint_error = subject.validate(content, nil)
121+
expect(lint_error.count).to eq(1)
122+
lint_error = lint_error[0]
123+
124+
expect(lint_error['source']).to eq('Puppet')
125+
expect(lint_error['message']).to match('A Mock Runtime Error')
126+
expect(lint_error['range']['start']['line']).to eq(10)
127+
expect(lint_error['range']['end']['line']).to eq(10)
128+
expect(lint_error['range']).to_not be_nil
129+
expect(lint_error['severity']).to eq(LanguageServer::DIAGNOSTICSEVERITY_ERROR)
130+
end
131+
end
132+
133+
134+
context 'with an unknown method in the Puppetfile' do
135+
let(:content) do <<-EOT
136+
forge 'https://forge.puppetlabs.com/'
137+
138+
# Modules from the Puppet Forge
139+
mod 'puppetlabs-somemodule', '1.0.0'
140+
141+
# Git style modules
142+
mod 'gitcommitmodule',
143+
:git => 'https://github.com/username/repo',
144+
:commit => 'abc123'
145+
mod_BROKEN 'gittagmodule',
146+
:git => 'https://github.com/username/repo',
147+
:tag => '0.1'
148+
EOT
149+
end
150+
151+
it 'should return a validation error on the specified line' do
152+
lint_error = subject.validate(content, nil)[0]
153+
154+
expect(lint_error['source']).to eq('Puppet')
155+
expect(lint_error['message']).to match('mod_BROKEN')
156+
expect(lint_error['range']['start']['line']).to eq(9)
157+
expect(lint_error['range']['end']['line']).to eq(9)
158+
expect(lint_error['severity']).to eq(LanguageServer::DIAGNOSTICSEVERITY_ERROR)
159+
end
160+
end
161+
end
162+
end

spec/languageserver/unit/puppet-languageserver/validation_queue_spec.rb

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
allow(PuppetLanguageServer::Manifest::ValidationProvider).to receive(:validate).and_raise("PuppetLanguageServer::Manifest::ValidationProvider.validate mock should not be called")
3232
allow(PuppetLanguageServer::Epp::ValidationProvider).to receive(:validate).and_raise("PuppetLanguageServer::Epp::ValidationProvider.validate mock should not be called")
33+
allow(PuppetLanguageServer::Puppetfile::ValidationProvider).to receive(:validate).and_raise("PuppetLanguageServer::Puppetfile::ValidationProvider.validate mock should not be called")
3334
end
3435

3536
context 'for an invalid or missing documents' do
@@ -56,24 +57,28 @@
5657

5758
it 'should only return the most recent validation results' do
5859
# Configure the document store
59-
subject.documents.set_document(MANIFEST_FILENAME, file_content0, document_version + 0)
60-
subject.documents.set_document(MANIFEST_FILENAME, file_content1, document_version + 1)
61-
subject.documents.set_document(MANIFEST_FILENAME, file_content3, document_version + 3)
62-
subject.documents.set_document(EPP_FILENAME, file_content1, document_version + 1)
60+
subject.documents.set_document(MANIFEST_FILENAME, file_content0, document_version + 0)
61+
subject.documents.set_document(MANIFEST_FILENAME, file_content1, document_version + 1)
62+
subject.documents.set_document(MANIFEST_FILENAME, file_content3, document_version + 3)
63+
subject.documents.set_document(EPP_FILENAME, file_content1, document_version + 1)
64+
subject.documents.set_document(PUPPETFILE_FILENAME, file_content1, document_version + 1)
6365

6466
# Preconfigure the validation queue
6567
subject.reset_queue([
66-
{ 'file_uri' => MANIFEST_FILENAME, 'doc_version' => document_version + 0, 'document_type' => :manifest, 'workspace' => workspace, 'connection_object' => connection },
67-
{ 'file_uri' => MANIFEST_FILENAME, 'doc_version' => document_version + 1, 'document_type' => :manifest, 'workspace' => workspace, 'connection_object' => connection },
68-
{ 'file_uri' => MANIFEST_FILENAME, 'doc_version' => document_version + 3, 'document_type' => :manifest, 'workspace' => workspace, 'connection_object' => connection },
69-
{ 'file_uri' => EPP_FILENAME, 'doc_version' => document_version + 1, 'document_type' => :epp, 'workspace' => workspace, 'connection_object' => connection },
68+
{ 'file_uri' => MANIFEST_FILENAME, 'doc_version' => document_version + 0, 'document_type' => :manifest, 'workspace' => workspace, 'connection_object' => connection },
69+
{ 'file_uri' => MANIFEST_FILENAME, 'doc_version' => document_version + 1, 'document_type' => :manifest, 'workspace' => workspace, 'connection_object' => connection },
70+
{ 'file_uri' => MANIFEST_FILENAME, 'doc_version' => document_version + 3, 'document_type' => :manifest, 'workspace' => workspace, 'connection_object' => connection },
71+
{ 'file_uri' => EPP_FILENAME, 'doc_version' => document_version + 1, 'document_type' => :epp, 'workspace' => workspace, 'connection_object' => connection },
72+
{ 'file_uri' => PUPPETFILE_FILENAME, 'doc_version' => document_version + 1, 'document_type' => :puppetfile, 'workspace' => workspace, 'connection_object' => connection },
7073
])
7174

7275
# We only expect the following results to be returned
7376
expect(PuppetLanguageServer::Manifest::ValidationProvider).to receive(:validate).with(file_content2, workspace).and_return(validation_result)
7477
expect(PuppetLanguageServer::Epp::ValidationProvider).to receive(:validate).with(file_content1, workspace).and_return(validation_result)
78+
expect(PuppetLanguageServer::Puppetfile::ValidationProvider).to receive(:validate).with(file_content1, workspace).and_return(validation_result)
7579
expect(connection).to receive(:reply_diagnostics).with(MANIFEST_FILENAME, validation_result)
7680
expect(connection).to receive(:reply_diagnostics).with(EPP_FILENAME, validation_result)
81+
expect(connection).to receive(:reply_diagnostics).with(PUPPETFILE_FILENAME, validation_result)
7782

7883
# Simulate a new document begin added by adding it to the document store and
7984
# enqueue validation for a version that it's in the middle of the versions in the queue
@@ -96,7 +101,11 @@
96101
end
97102

98103
context 'of a Puppetfile file' do
99-
validation_result = []
104+
validation_result = [{ 'result' => 'MockResult' }]
105+
106+
before(:each) do
107+
expect(PuppetLanguageServer::Puppetfile::ValidationProvider).to receive(:validate).with(FILE_CONTENT, workspace).and_return(validation_result)
108+
end
100109

101110
it_should_behave_like "single document which sends validation results", PUPPETFILE_FILENAME, FILE_CONTENT, validation_result
102111
end
@@ -134,6 +143,7 @@
134143

135144
allow(PuppetLanguageServer::Manifest::ValidationProvider).to receive(:validate).and_raise("PuppetLanguageServer::Manifest::ValidationProvider.validate mock should not be called")
136145
allow(PuppetLanguageServer::Epp::ValidationProvider).to receive(:validate).and_raise("PuppetLanguageServer::Epp::ValidationProvider.validate mock should not be called")
146+
allow(PuppetLanguageServer::Puppetfile::ValidationProvider).to receive(:validate).and_raise("PuppetLanguageServer::Puppetfile::ValidationProvider.validate mock should not be called")
137147
end
138148

139149
it 'should not send validation results for documents that do not exist' do
@@ -153,7 +163,11 @@
153163
end
154164

155165
context 'for a Puppetfile file' do
156-
validation_result = []
166+
validation_result = [{ 'result' => 'MockResult' }]
167+
168+
before(:each) do
169+
expect(PuppetLanguageServer::Puppetfile::ValidationProvider).to receive(:validate).with(FILE_CONTENT, workspace).and_return(validation_result)
170+
end
157171

158172
it_should_behave_like "document which sends validation results", PUPPETFILE_FILENAME, FILE_CONTENT, validation_result
159173
end

0 commit comments

Comments
 (0)