Skip to content

Commit e784a13

Browse files
committed
(GH-94) Use bolt metadata when inside plans
Previously the language server had no concept of Bolt Plan specific metadata, e.g. Functions and DataTypes. This commit updates the providers to optionally query for Bolt specific (taskmode => true) metadata when gathering results.
1 parent 547d0fe commit e784a13

File tree

6 files changed

+137
-22
lines changed

6 files changed

+137
-22
lines changed

lib/puppet-languageserver/manifest/completion_provider.rb

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def self.complete(content, line_num, char_num, options = {})
2424
# Add resources
2525
all_resources { |x| items << x }
2626

27-
all_functions { |x| items << x }
27+
all_functions(options[:tasks_mode]) { |x| items << x }
2828

2929
response = LSP::CompletionList.new
3030
response.items = items
@@ -50,6 +50,14 @@ def self.complete(content, line_num, char_num, options = {})
5050
# Add resources
5151
all_resources { |x| items << x }
5252

53+
when 'Puppet::Pops::Model::PlanDefinition'
54+
# We are in the root of a `plan` statement
55+
56+
# Add resources
57+
all_resources { |x| items << x }
58+
59+
all_functions(options[:tasks_mode]) { |x| items << x }
60+
5361
when 'Puppet::Pops::Model::ResourceExpression'
5462
# We are inside a resource definition. Should display all available
5563
# properities and parameters.
@@ -173,8 +181,8 @@ def self.all_resources(&block)
173181
end
174182
end
175183

176-
def self.all_functions(&block)
177-
PuppetLanguageServer::PuppetHelper.function_names.each do |name|
184+
def self.all_functions(tasks_mode, &block)
185+
PuppetLanguageServer::PuppetHelper.function_names(tasks_mode).each do |name|
178186
item = LSP::CompletionItem.new(
179187
'label' => name.to_s,
180188
'kind' => LSP::CompletionItemKind::FUNCTION,
@@ -231,7 +239,8 @@ def self.resolve(completion_item)
231239
end
232240

233241
when 'function'
234-
item_type = PuppetLanguageServer::PuppetHelper.function(data['name'])
242+
# We don't know if this resolution is coming from a plan or not, so just assume it is
243+
item_type = PuppetLanguageServer::PuppetHelper.function(data['name'], true)
235244
return result if item_type.nil?
236245
result.documentation = item_type.doc unless item_type.doc.nil?
237246
unless item_type.nil? || item_type.signatures.count.zero?

lib/puppet-languageserver/manifest/hover_provider.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def self.resolve(content, line_num, char_num, options = {})
3333

3434
content = get_hover_content_for_access_expression(path, expr)
3535
when 'Puppet::Pops::Model::CallNamedFunctionExpression'
36-
content = get_call_named_function_expression_content(item)
36+
content = get_call_named_function_expression_content(item, options[:tasks_mode])
3737
when 'Puppet::Pops::Model::AttributeOperation'
3838
# Get the parent resource class
3939
distance_up_ast = -1
@@ -66,7 +66,7 @@ def self.resolve(content, line_num, char_num, options = {})
6666
# https://github.com/puppetlabs/puppet-specifications/blob/master/language/names.md#names
6767
# Datatypes have to start with uppercase and can be fully qualified
6868
if item.cased_value =~ /^[A-Z][a-zA-Z:0-9]*$/ # rubocop:disable Style/GuardClause
69-
content = get_puppet_datatype_content(item)
69+
content = get_puppet_datatype_content(item, options[:tasks_mode])
7070
else
7171
raise "#{item.cased_value} is an unknown QualifiedReference"
7272
end
@@ -143,10 +143,10 @@ def self.get_attribute_class_parameter_content(item_class, param)
143143
content
144144
end
145145

146-
def self.get_call_named_function_expression_content(item)
146+
def self.get_call_named_function_expression_content(item, tasks_mode)
147147
func_name = item.functor_expr.value
148148

149-
func_info = PuppetLanguageServer::PuppetHelper.function(func_name)
149+
func_info = PuppetLanguageServer::PuppetHelper.function(func_name, tasks_mode)
150150
raise "Function #{func_name} does not exist" if func_info.nil?
151151

152152
content = "**#{func_name}** Function"
@@ -189,8 +189,8 @@ def self.get_puppet_class_content(item_class)
189189
end
190190
private_class_method :get_puppet_class_content
191191

192-
def self.get_puppet_datatype_content(item)
193-
dt_info = PuppetLanguageServer::PuppetHelper.datatype(item.cased_value)
192+
def self.get_puppet_datatype_content(item, tasks_mode)
193+
dt_info = PuppetLanguageServer::PuppetHelper.datatype(item.cased_value, tasks_mode)
194194
raise "DataType #{item.cased_value} does not exist" if dt_info.nil?
195195

196196
content = "**#{item.cased_value}** Data Type"

lib/puppet-languageserver/puppet_helper.rb

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,18 +160,25 @@ def self.filtered_function_names(&block)
160160
result
161161
end
162162

163-
def self.function(name)
163+
def self.function(name, tasks_mode = false)
164164
return nil if @default_functions_loaded == false
165165
raise('Puppet Helper Cache has not been configured') if @inmemory_cache.nil?
166166
load_default_functions unless @default_functions_loaded
167-
@inmemory_cache.object_by_name(:function, name)
167+
exclude_origins = tasks_mode ? [] : [:bolt]
168+
@inmemory_cache.object_by_name(
169+
:function,
170+
name,
171+
:fuzzy_match => true,
172+
:exclude_origins => exclude_origins
173+
)
168174
end
169175

170-
def self.function_names
176+
def self.function_names(tasks_mode = false)
171177
return [] if @default_functions_loaded == false
172178
raise('Puppet Helper Cache has not been configured') if @inmemory_cache.nil?
173179
load_default_functions if @default_functions_loaded.nil?
174-
@inmemory_cache.object_names_by_section(:function).map(&:to_s)
180+
exclude_origins = tasks_mode ? [] : [:bolt]
181+
@inmemory_cache.object_names_by_section(:function, :exclude_origins => exclude_origins).map(&:to_s)
175182
end
176183

177184
# Classes and Defined Types
@@ -229,11 +236,18 @@ def self.load_default_datatypes_async
229236
sidecar_queue.enqueue('default_datatypes', [])
230237
end
231238

232-
def self.datatype(name)
239+
def self.datatype(name, tasks_mode = false)
233240
return nil if @default_datatypes_loaded == false
234241
raise('Puppet Helper Cache has not been configured') if @inmemory_cache.nil?
235242
load_default_datatypes unless @default_datatypes_loaded
236-
@inmemory_cache.object_by_name(:datatype, name)
243+
load_static_data if tasks_mode && !static_data_loaded?
244+
exclude_origins = tasks_mode ? [] : [:bolt]
245+
@inmemory_cache.object_by_name(
246+
:datatype,
247+
name,
248+
:fuzzy_match => true,
249+
:exclude_origins => exclude_origins
250+
)
237251
end
238252

239253
def self.cache

lib/puppet-languageserver/puppet_helper/cache.rb

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,60 @@ def remove_section!(section, origin = nil)
3636
end
3737

3838
# section => <Type of object in the file :function, :type, :class, :datatype>
39-
def object_by_name(section, name)
39+
def object_by_name(section, name, options = {})
40+
# options[:exclude_origins]
41+
# options[:fuzzy_match]
42+
options = {
43+
:exclude_origins => [],
44+
:fuzzy_match => false
45+
}.merge(options)
46+
4047
name = name.intern if name.is_a?(String)
4148
return nil if section.nil?
4249
@cache_lock.synchronize do
43-
@inmemory_cache.each do |_, sections|
50+
@inmemory_cache.each do |origin, sections|
4451
next if sections[section].nil? || sections[section].empty?
52+
next if options[:exclude_origins].include?(origin)
4553
sections[section].each do |item|
46-
return item if item.key == name
54+
match = options[:fuzzy_match] ? fuzzy_match?(item.key, name) : item.key == name
55+
return item if match
4756
end
4857
end
4958
end
5059
nil
5160
end
5261

62+
# Performs fuzzy text matching of Puppet Language Type names
63+
# e.g 'TargetSpec' in 'Boltlib::TargetSpec'
64+
# @api private
65+
def fuzzy_match?(obj, test_obj)
66+
value = obj.is_a?(String) ? obj.dup : obj.to_s
67+
test_string = test_obj.is_a?(String) ? test_obj.dup : test_obj.to_s
68+
69+
# Test for equality
70+
return true if value == test_string
71+
72+
# Test for a shortname
73+
unless test_string.start_with?('::')
74+
# e.g 'TargetSpec' in 'Boltlib::TargetSpec'
75+
return true if value.end_with?('::' + test_string)
76+
end
77+
78+
false
79+
end
80+
5381
# section => <Type of object in the file :function, :type, :class, :datatype>
54-
def object_names_by_section(section)
82+
# options[:exclude_origins]
83+
def object_names_by_section(section, options = {})
84+
options = {
85+
:exclude_origins => []
86+
}.merge(options)
5587
result = []
5688
return result if section.nil?
5789
@cache_lock.synchronize do
58-
@inmemory_cache.each do |_, sections|
90+
@inmemory_cache.each do |origin, sections|
5991
next if sections[section].nil? || sections[section].empty?
92+
next if options[:exclude_origins].include?(origin)
6093
result.concat(sections[section].map { |i| i.key })
6194
end
6295
end

spec/languageserver/integration/puppet-languageserver/manifest/completion_provider_spec.rb

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ def number_of_completion_item_with_type(completion_list, typename)
44
(completion_list.items.select { |item| item.data['type'] == typename}).length
55
end
66

7+
def completion_item_with_type_and_name(completion_list, typename, name)
8+
completion_list.items.find { |item| item.data['type'] == typename && item.data['name'] == name }
9+
end
10+
711
def retrieve_completion_response(label, kind)
812
value = @completion_response.items.find do |item|
913
item.label == label && item.kind == kind
@@ -106,12 +110,20 @@ def create_ensurable_property
106110
let(:content) { <<-EOT
107111
plan mymodule::my_plan(
108112
) {
113+
# Needed
109114
}
110115
EOT
111116
}
117+
112118
it "should not raise an error" do
113119
result = subject.complete(content, 0, 1, { :tasks_mode => true})
114120
end
121+
122+
it 'should suggest Bolt functions' do
123+
result = subject.complete(content, 2, 1, { :tasks_mode => true})
124+
125+
expect(completion_item_with_type_and_name(result, 'function', 'run_task')).to_not be_nil
126+
end
115127
end
116128

117129
context "Given a simple valid manifest" do
@@ -147,7 +159,7 @@ class Alice {
147159
end
148160

149161
expected_types.each do |typename|
150-
expect(number_of_completion_item_with_type(result,typename)).to be > 0
162+
expect(number_of_completion_item_with_type(result, typename)).to be > 0
151163
end
152164
end
153165
end
@@ -464,6 +476,21 @@ class Alice {
464476
expect(result.insertTextFormat).to eq(LSP::InsertTextFormat::PLAINTEXT)
465477
end
466478
end
479+
480+
context 'for a Bolt function (run_task)' do
481+
it 'should return the documentation' do
482+
@resolve_request.data['name'] = 'run_task'
483+
result = subject.resolve(@resolve_request)
484+
expect(result.documentation).to match(/.+/)
485+
end
486+
487+
it 'should return plain text' do
488+
@resolve_request.data['name'] = 'run_task'
489+
result = subject.resolve(@resolve_request)
490+
expect(result.insertText).to match(/.+/)
491+
expect(result.insertTextFormat).to eq(LSP::InsertTextFormat::PLAINTEXT)
492+
end
493+
end
467494
end
468495

469496
context 'when resolving a resource_type request' do

spec/languageserver/integration/puppet-languageserver/manifest/hover_provider_spec.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,45 @@
7575
context 'Given a Puppet Plan', :if => Puppet.tasks_supported? do
7676
let(:content) { <<-EOT
7777
plan mymodule::my_plan(
78+
TargetSpec $webservers,
7879
) {
80+
$webserver_names = get_targets($webservers).map |$n| { $n.name }
7981
}
8082
EOT
8183
}
84+
8285
it "should not raise an error" do
8386
result = subject.resolve(content, 0, 1, { :tasks_mode => true})
8487
end
88+
89+
it 'should find bolt specific data types' do
90+
result = subject.resolve(content, 1, 15, { :tasks_mode => true})
91+
expect(result.contents).to start_with("**TargetSpec** Data Type Alias\n")
92+
end
93+
94+
it 'should find bolt specific functions' do
95+
result = subject.resolve(content, 3, 36, { :tasks_mode => true})
96+
expect(result.contents).to start_with("**get_targets** Function\n")
97+
end
98+
end
99+
100+
context 'When using Bolt specific information in a normal manifest' do
101+
let(:content) { <<-EOT
102+
class mymodule::my_plan(
103+
TargetSpec $webservers,
104+
) {
105+
$webserver_names = get_targets($webservers).map |$n| { $n.name }
106+
}
107+
EOT
108+
}
109+
110+
it "should raise an error for Bolt datatypes" do
111+
expect{subject.resolve(content, 1, 15, { :tasks_mode => false})}.to raise_error(RuntimeError)
112+
end
113+
114+
it "should raise an error for Bolt functions" do
115+
expect{subject.resolve(content, 3, 36, { :tasks_mode => false})}.to raise_error(RuntimeError)
116+
end
85117
end
86118

87119
describe 'when cursor is in the root of the document' do

0 commit comments

Comments
 (0)