Skip to content

Commit 705303d

Browse files
committed
(GH-166) Add find definition for types and functions
Previously the Language Server did not respond to find definition requests from the client. This commit * Adds a request handler for the textDocument/definition request * A definition provider class which will extract the definition * Monkey patch Type and Function loading so the language server can determine where types and functions came from * Added definition provider as a language server capability This commit adds parsing of puppet manifests for classes and defined types for manifests that exist in the module path. * The classes are loading async if preload is configured * Will respond to definition requests if a type with the same name could not be found
1 parent 817fb03 commit 705303d

File tree

8 files changed

+346
-5
lines changed

8 files changed

+346
-5
lines changed

server/lib/languageserver/languageserver.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
%w[constants diagnostic completion_list completion_item hover puppet_version puppet_compilation].each do |lib|
1+
%w[constants diagnostic completion_list completion_item hover location puppet_version puppet_compilation].each do |lib|
22
begin
33
require "languageserver/#{lib}"
44
rescue LoadError
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
module LanguageServer
2+
# /**
3+
# * The result of a hover request.
4+
# */
5+
# export interface Location {
6+
# uri: string;
7+
# range: Range;
8+
# }
9+
10+
module Location
11+
def self.create(options)
12+
result = {}
13+
raise('uri is a required field for Location') if options['uri'].nil?
14+
15+
result['uri'] = options['uri']
16+
result['range'] = {
17+
'start' => {
18+
'line' => options['fromline'],
19+
'character' => options['fromchar']
20+
},
21+
'end' => {
22+
'line' => options['toline'],
23+
'character' => options['tochar']
24+
}
25+
}
26+
27+
result
28+
end
29+
end
30+
end

server/lib/puppet-languageserver.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
require 'languageserver/languageserver'
66
require 'puppet-vscode'
77

8-
%w[json_rpc_handler message_router server_capabilities document_validator
9-
puppet_parser_helper puppet_helper facter_helper completion_provider hover_provider].each do |lib|
8+
%w[json_rpc_handler message_router server_capabilities document_validator puppet_parser_helper puppet_helper
9+
facter_helper completion_provider hover_provider definition_provider puppet_monkey_patches].each do |lib|
1010
begin
1111
require "puppet-languageserver/#{lib}"
1212
rescue LoadError
@@ -126,6 +126,9 @@ def self.init_puppet_worker(options)
126126

127127
log_message(:info, 'Preloading Functions (Async)...')
128128
PuppetLanguageServer::PuppetHelper.load_functions_async
129+
130+
log_message(:info, 'Preloading Classes (Async)...')
131+
PuppetLanguageServer::PuppetHelper.load_classes_async
129132
else
130133
log_message(:info, 'Skipping preloading Puppet')
131134
end
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
module PuppetLanguageServer
2+
module DefinitionProvider
3+
def self.find_definition(content, line_num, char_num)
4+
result = PuppetLanguageServer::PuppetParserHelper.object_under_cursor(content, line_num, char_num, false, [Puppet::Pops::Model::BlockExpression])
5+
6+
return nil if result.nil?
7+
8+
path = result[:path]
9+
item = result[:model]
10+
11+
response = []
12+
case item.class.to_s
13+
when 'Puppet::Pops::Model::CallNamedFunctionExpression'
14+
func_name = item.functor_expr.value
15+
response << function_name(resource_name)
16+
17+
when 'Puppet::Pops::Model::QualifiedName'
18+
# Qualified names could be anything. Context is the key here
19+
parent = path.last
20+
21+
# What if it's a function name. Then the Qualified name must be the same as the function name
22+
if !parent.nil? &&
23+
parent.class.to_s == 'Puppet::Pops::Model::CallNamedFunctionExpression' &&
24+
parent.functor_expr.value == item.value
25+
func_name = item.value
26+
response << function_name(func_name)
27+
end
28+
# What if it's an "include <class>" call
29+
if !parent.nil? && parent.class.to_s == 'Puppet::Pops::Model::CallNamedFunctionExpression' && parent.functor_expr.value == 'include'
30+
resource_name = item.value
31+
response << type_or_class(resource_name)
32+
end
33+
# What if it's the name of a resource type or class
34+
if !parent.nil? && parent.class.to_s == 'Puppet::Pops::Model::ResourceExpression'
35+
resource_name = item.value
36+
response << type_or_class(resource_name)
37+
end
38+
39+
when 'Puppet::Pops::Model::ResourceExpression'
40+
resource_name = item.type_name.value
41+
response << type_or_class(resource_name)
42+
43+
else
44+
raise "Unable to generate Defintion information for object of type #{item.class}"
45+
end
46+
47+
response.compact
48+
end
49+
50+
private
51+
def self.type_or_class(resource_name)
52+
# Strip the leading double-colons for root resource names
53+
resource_name = resource_name.slice(2, resource_name.length - 2) if resource_name.start_with?('::')
54+
location = PuppetLanguageServer::PuppetHelper.type_load_info(resource_name)
55+
location = PuppetLanguageServer::PuppetHelper.class_load_info(resource_name) if location.nil?
56+
unless location.nil?
57+
return LanguageServer::Location.create({
58+
'uri' => 'file:///' + location['source'],
59+
'fromline' => location['line'],
60+
'fromchar' => 0,
61+
'toline' => location['line'],
62+
'tochar' => 1024,
63+
})
64+
end
65+
nil
66+
end
67+
68+
def self.function_name(func_name)
69+
location = PuppetLanguageServer::PuppetHelper.function_load_info(func_name)
70+
unless location.nil?
71+
return LanguageServer::Location.create({
72+
'uri' => 'file:///' + location['source'],
73+
'fromline' => location['line'],
74+
'fromchar' => 0,
75+
'toline' => location['line'],
76+
'tochar' => 1024,
77+
})
78+
end
79+
nil
80+
end
81+
82+
end
83+
end

server/lib/puppet-languageserver/message_router.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,20 @@ def receive_request(request)
184184
PuppetLanguageServer.log_message(:error, "(textDocument/hover) #{exception}")
185185
request.reply_result(LanguageServer::Hover.create_nil_response)
186186
end
187+
188+
when 'textDocument/definition'
189+
file_uri = request.params['textDocument']['uri']
190+
line_num = request.params['position']['line']
191+
char_num = request.params['position']['character']
192+
content = documents.document(file_uri)
193+
begin
194+
#raise "Not Implemented"
195+
request.reply_result(PuppetLanguageServer::DefinitionProvider.find_definition(content, line_num, char_num))
196+
rescue StandardError => exception
197+
PuppetLanguageServer.log_message(:error, "(textDocument/definition) #{exception}")
198+
request.reply_result($null)
199+
end
200+
187201
else
188202
PuppetLanguageServer.log_message(:error, "Unknown RPC method #{request.rpc_method}")
189203
end

server/lib/puppet-languageserver/puppet_helper.rb

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
require 'puppet/indirector/face'
2-
2+
require 'pathname'
33
module PuppetLanguageServer
44
module PuppetHelper
55
# Reference - https://github.com/puppetlabs/puppet/blob/master/lib/puppet/reference/type.rb
66

77
@ops_lock_types = Mutex.new
88
@ops_lock_funcs = Mutex.new
9+
@ops_lock_classes = Mutex.new
910
@types_hash = nil
1011
@function_module = nil
1112
@types_loaded = nil
1213
@functions_loaded = nil
14+
@classes_loaded = nil
15+
@class_load_info = {}
16+
@function_load_info = {}
17+
@type_load_info = {}
1318

1419
def self.reset
1520
@ops_lock_types.synchronize do
@@ -114,13 +119,60 @@ def self.function_names
114119
result
115120
end
116121

122+
# Classes and Defined Types
123+
def self.classes_loaded?
124+
@classes_loaded.nil? ? false : @classes_loaded
125+
end
126+
127+
def self.load_classes
128+
@ops_lock_classes.synchronize do
129+
_load_classes if @classes_loaded.nil?
130+
end
131+
end
132+
133+
def self.load_classes_async
134+
Thread.new do
135+
load_classes
136+
end
137+
end
138+
139+
# Loading information
140+
def self.add_function_load_info(name, options)
141+
@function_load_info[name.to_s] = options
142+
end
143+
144+
def self.function_load_info(name)
145+
options = @function_load_info[name.to_s]
146+
options.nil? ? nil : options.dup
147+
end
148+
149+
def self.add_type_load_info(name, options)
150+
@type_load_info[name.to_s] = options
151+
end
152+
153+
def self.type_load_info(name)
154+
options = @type_load_info[name.to_s]
155+
options.nil? ? nil : options.dup
156+
end
157+
158+
def self.class_load_info(name)
159+
# This is the only entrypoint to class loading
160+
load_classes
161+
options = @class_load_info[name.to_s]
162+
options.nil? ? nil : options.dup
163+
end
164+
117165
# DO NOT ops_lock on any of these methods
118166
# deadlocks will ensue!
119167
def self._reset
120168
@types_hash = nil
121169
@function_module = nil
122170
@types_loaded = nil
123171
@functions_loaded = nil
172+
@classes_loaded = nil
173+
@function_load_info = {}
174+
@type_load_info = {}
175+
@class_load_info = {}
124176
end
125177
private_class_method :_reset
126178

@@ -134,6 +186,126 @@ def self.prune_resource_parameters(resources)
134186
end
135187
private_class_method :prune_resource_parameters
136188

189+
# Class and Defined Type loading
190+
def self._load_classes
191+
@classes_loaded = false
192+
module_path_list = []
193+
# Add the base modulepath
194+
module_path_list.concat(Puppet::Node::Environment.split_path(Puppet.settings[:basemodulepath]))
195+
# Add the modulepath
196+
module_path_list.concat(Puppet::Node::Environment.split_path(Puppet.settings[:modulepath]))
197+
198+
# Add the environment specified in puppet conf - This can be overridden by the master but there's no way to know.
199+
unless Puppet.settings[:environmentpath].nil?
200+
module_path_list << File.join(Puppet.settings[:environmentpath], Puppet.settings[:environment], 'modules') unless Puppet.settings[:environment].nil?
201+
202+
module_path_list.concat(Pathname.new(Puppet.settings[:environmentpath])
203+
.children
204+
.select { |c| c.directory? }
205+
.collect { |c| File.join(c, 'modules') }
206+
)
207+
end
208+
module_path_list.uniq!
209+
PuppetLanguageServer.log_message(:debug, "[PuppetHelper::_load_classes] Loading classes from #{module_path_list}")
210+
211+
# Find all of the manifest paths for all of the modules...
212+
manifest_path_list = []
213+
module_path_list.each do |module_path|
214+
next unless File.exists?(module_path)
215+
Pathname.new(module_path)
216+
.children
217+
.select { |c| c.directory? }
218+
.each do |module_filepath|
219+
manifest_path = File.join(module_filepath,'manifests')
220+
manifest_path_list << manifest_path if File.exists?(manifest_path)
221+
end
222+
end
223+
224+
# Find and parse all manifests in the manifest paths
225+
@class_load_info = {}
226+
manifest_path_list.each do |manifest_path|
227+
Dir.glob("#{manifest_path}/**/*.pp").each do |manifest_file|
228+
classes = load_classes_from_manifest(manifest_file)
229+
next if classes.nil?
230+
classes.each do |key, data|
231+
@class_load_info[key] = data unless @class_load_info.has_key?(name)
232+
end
233+
end
234+
end
235+
@classes_loaded = true
236+
237+
PuppetLanguageServer.log_message(:debug, "[PuppetHelper::_load_classes] Finished loading #{@class_load_info.count} classes")
238+
nil
239+
end
240+
private_class_method :_load_classes
241+
242+
def self.load_classes_from_manifest(manifest_file)
243+
file_content = File.open(manifest_file, "r:UTF-8") { |f| f.read }
244+
245+
parser = Puppet::Pops::Parser::Parser.new
246+
result = nil
247+
begin
248+
result = parser.parse_string(file_content, '')
249+
rescue Puppet::ParseErrorWithIssue => _exception
250+
# Any parsing errors means we can't inspect the document
251+
return nil
252+
end
253+
254+
class_info = {}
255+
# Enumerate the entire AST looking for classes and defined types
256+
# TODO - Need to learn how to read the help/docs for hover support
257+
if result.model.respond_to? :eAllContents
258+
# TODO Puppet 4 language stuff
259+
result.model.eAllContents.select do |item|
260+
case item.class.to_s
261+
when 'Puppet::Pops::Model::HostClassDefinition'
262+
class_info[item.name] = {
263+
'name' => item.name,
264+
'type' => 'class',
265+
'parameters' => item.parameters,
266+
'source' => manifest_file,
267+
'line' => result.locator.line_for_offset(item.offset) - 1,
268+
'char' => result.locator.offset_on_line(item.offset)
269+
}
270+
when 'Puppet::Pops::Model::ResourceTypeDefinition'
271+
class_info[item.name] = {
272+
'name' => item.name,
273+
'type' => 'typedefinition',
274+
'parameters' => item.parameters,
275+
'source' => manifest_file,
276+
'line' => result.locator.line_for_offset(item.offset) - 1,
277+
'char' => result.locator.offset_on_line(item.offset)
278+
}
279+
end
280+
end
281+
else
282+
result.model._pcore_all_contents([]) do |item|
283+
case item.class.to_s
284+
when 'Puppet::Pops::Model::HostClassDefinition'
285+
class_info[item.name] = {
286+
'name' => item.name,
287+
'type' => 'class',
288+
'parameters' => item.parameters,
289+
'source' => manifest_file,
290+
'line' => item.line,
291+
'char' => item.pos
292+
}
293+
when 'Puppet::Pops::Model::ResourceTypeDefinition'
294+
class_info[item.name] = {
295+
'name' => item.name,
296+
'type' => 'typedefinition',
297+
'parameters' => item.parameters,
298+
'source' => manifest_file,
299+
'line' => item.line,
300+
'char' => item.pos
301+
}
302+
end
303+
end
304+
end
305+
306+
class_info
307+
end
308+
137309
def self._load_types
138310
@types_loaded = false
139311
@types_hash = {}

0 commit comments

Comments
 (0)