Skip to content

Commit 538c21c

Browse files
committed
(GH-24) Allow parsing of manifests in tasks mode
The Puppet language introduced Puppet Tasks and Plans as part of Puppet 5.4.0 [1]. A special parsing switch `--tasks` was introduced which changes the behaviour of the Puppet parser, that is switching it into "tasks" mode. Unfortunately this switch is global to all Puppet parsing therefore to dynamically switch it like we do in Editor Services, this required a global mutex to be used. Note that this commit does not add Puppet Tasks/Plans intellisense but merely allows the standard manifest providers (completion, validation etc.) to actually function instead of silently failing This commit; * Modifies the Puppet parser, via monkey patching, to implement a "singleton" parsing method that dynamically, and safely, switch the parsing mode on a per request basis * Modifies the Document Store to detect whether a File URI is a Puppet Task/Plan based on the relative path of the file versus the module root. * Modifies the various providers to call the parsing helper in the correct mode * Modifies the Message Router to detect whether a file is a Puppet Task/Plan and then sets the appropriate mode when calling the manifest providers (completion, validation etc.) [1] https://puppet.com/docs/bolt/1.x/writing_plans.html#concept-4485
1 parent 6758afa commit 538c21c

20 files changed

+445
-43
lines changed

lib/puppet-languageserver/document_store.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ def self.document_type(uri)
5252
end
5353
end
5454

55+
# Plan files https://puppet.com/docs/bolt/1.x/writing_plans.html#concept-4485
56+
# exist in modules (requires metadata.json) and are in the `/plans` directory
57+
def self.module_plan_file?(uri)
58+
return false unless store_has_module_metadata?
59+
relative_path = PuppetLanguageServer::UriHelper.relative_uri_path(PuppetLanguageServer::UriHelper.build_file_uri(store_root_path), uri, !windows?)
60+
return false if relative_path.nil?
61+
relative_path.start_with?('/plans/')
62+
end
63+
5564
# Workspace management
5665
WORKSPACE_CACHE_TTL_SECONDS = 60
5766
def self.initialize_store(options = {})
@@ -150,5 +159,12 @@ def self.dir_exist?(path)
150159
Dir.exist?(path)
151160
end
152161
private_class_method :dir_exist?
162+
163+
def self.windows?
164+
# Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard
165+
# library uses that to test what platform it's on.
166+
!!File::ALT_SEPARATOR # rubocop:disable Style/DoubleNegation
167+
end
168+
private_class_method :windows?
153169
end
154170
end

lib/puppet-languageserver/manifest/completion_provider.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
module PuppetLanguageServer
22
module Manifest
33
module CompletionProvider
4-
def self.complete(content, line_num, char_num)
4+
def self.complete(content, line_num, char_num, options = {})
5+
options = {
6+
:tasks_mode => false
7+
}.merge(options)
58
items = []
69
incomplete = false
710

8-
result = PuppetLanguageServer::PuppetParserHelper.object_under_cursor(content, line_num, char_num, true, [Puppet::Pops::Model::QualifiedName, Puppet::Pops::Model::BlockExpression])
9-
11+
result = PuppetLanguageServer::PuppetParserHelper.object_under_cursor(content, line_num, char_num,
12+
:multiple_attempts => true,
13+
:disallowed_classes => [Puppet::Pops::Model::QualifiedName, Puppet::Pops::Model::BlockExpression],
14+
:tasks_mode => options[:tasks_mode])
1015
if result.nil?
1116
# We are in the root of the document.
1217

1318
# Add keywords
1419
keywords(%w[class define node application site]) { |x| items << x }
20+
keywords(%w[plan]) { |x| items << x } if options[:tasks_mode]
1521

1622
# Add resources
1723
all_resources { |x| items << x }

lib/puppet-languageserver/manifest/definition_provider.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
module PuppetLanguageServer
22
module Manifest
33
module DefinitionProvider
4-
def self.find_definition(content, line_num, char_num)
5-
result = PuppetLanguageServer::PuppetParserHelper.object_under_cursor(content, line_num, char_num, false, [Puppet::Pops::Model::BlockExpression])
6-
4+
def self.find_definition(content, line_num, char_num, options = {})
5+
options = {
6+
:tasks_mode => false
7+
}.merge(options)
8+
result = PuppetLanguageServer::PuppetParserHelper.object_under_cursor(content, line_num, char_num,
9+
:disallowed_classes => [Puppet::Pops::Model::BlockExpression],
10+
:tasks_mode => options[:tasks_mode])
711
return nil if result.nil?
812

913
path = result[:path]

lib/puppet-languageserver/manifest/document_symbol_provider.rb

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def self.workspace_symbols(query)
1717
'fromline' => item.line,
1818
'fromchar' => 0, # Don't have char pos for types
1919
'toline' => item.line,
20-
'tochar' => 1024, # Don't have char pos for types
20+
'tochar' => 1024 # Don't have char pos for types
2121
)
2222
)
2323

@@ -30,7 +30,7 @@ def self.workspace_symbols(query)
3030
'fromline' => item.line,
3131
'fromchar' => 0, # Don't have char pos for functions
3232
'toline' => item.line,
33-
'tochar' => 1024, # Don't have char pos for functions
33+
'tochar' => 1024 # Don't have char pos for functions
3434
)
3535
)
3636

@@ -43,17 +43,20 @@ def self.workspace_symbols(query)
4343
'fromline' => item.line,
4444
'fromchar' => 0, # Don't have char pos for classes
4545
'toline' => item.line,
46-
'tochar' => 1024, # Don't have char pos for classes
46+
'tochar' => 1024 # Don't have char pos for classes
4747
)
4848
)
4949
end
5050
end
5151
result
5252
end
5353

54-
def self.extract_document_symbols(content)
54+
def self.extract_document_symbols(content, options = {})
55+
options = {
56+
:tasks_mode => false
57+
}.merge(options)
5558
parser = Puppet::Pops::Parser::Parser.new
56-
result = parser.parse_string(content, '')
59+
result = parser.singleton_parse_string(content, options[:tasks_mode], '')
5760

5861
if result.model.respond_to? :eAllContents
5962
# We are unable to build a document symbol tree for Puppet 4 AST
@@ -187,6 +190,28 @@ def self.recurse_document_symbols(object, path, parentsymbol, symbollist)
187190
'children' => []
188191
)
189192

193+
# Puppet Plan
194+
when 'Puppet::Pops::Model::PlanDefinition'
195+
this_symbol = LanguageServer::DocumentSymbol.create(
196+
'name' => object.name,
197+
'kind' => LanguageServer::SYMBOLKIND_CLASS,
198+
'detail' => object.name,
199+
'range' => create_range_array(object.offset, object.length, object.locator),
200+
'selectionRange' => create_range_array(object.offset, object.length, object.locator),
201+
'children' => []
202+
)
203+
# Load in the class parameters
204+
object.parameters.each do |param|
205+
param_symbol = LanguageServer::DocumentSymbol.create(
206+
'name' => '$' + param.name,
207+
'kind' => LanguageServer::SYMBOLKIND_PROPERTY,
208+
'detail' => '$' + param.name,
209+
'range' => create_range_array(param.offset, param.length, param.locator),
210+
'selectionRange' => create_range_array(param.offset, param.length, param.locator),
211+
'children' => []
212+
)
213+
this_symbol['children'].push(param_symbol)
214+
end
190215
end
191216

192217
object._pcore_contents do |item|

lib/puppet-languageserver/manifest/hover_provider.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
module PuppetLanguageServer
22
module Manifest
33
module HoverProvider
4-
def self.resolve(content, line_num, char_num)
5-
result = PuppetLanguageServer::PuppetParserHelper.object_under_cursor(content, line_num, char_num, false, [Puppet::Pops::Model::QualifiedName, Puppet::Pops::Model::BlockExpression])
4+
def self.resolve(content, line_num, char_num, options = {})
5+
options = {
6+
:tasks_mode => false
7+
}.merge(options)
8+
result = PuppetLanguageServer::PuppetParserHelper.object_under_cursor(content, line_num, char_num,
9+
:disallowed_classes => [Puppet::Pops::Model::QualifiedName, Puppet::Pops::Model::BlockExpression],
10+
:tasks_mode => options[:tasks_mode])
611
return LanguageServer::Hover.create_nil_response if result.nil?
712

813
path = result[:path]

lib/puppet-languageserver/manifest/validation_provider.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ def self.fix_validate_errors(content)
2828
[problems_fixed, linter.manifest]
2929
end
3030

31-
def self.validate(content, _max_problems = 100)
31+
def self.validate(content, options = {})
32+
options = {
33+
:max_problems => 100,
34+
:tasks_mode => false
35+
}.merge(options)
36+
3237
result = []
3338
# TODO: Need to implement max_problems
3439
problems = 0
@@ -89,8 +94,16 @@ def self.validate(content, _max_problems = 100)
8994
Puppet.override({ loaders: loaders }, 'For puppet parser validate') do
9095
begin
9196
validation_environment = env
92-
validation_environment.check_for_reparse
93-
validation_environment.known_resource_types.clear
97+
$PuppetParserMutex.synchronize do # rubocop:disable Style/GlobalVars
98+
begin
99+
original_taskmode = Puppet[:tasks] if Puppet.tasks_supported?
100+
Puppet[:tasks] = options[:tasks_mode] if Puppet.tasks_supported?
101+
validation_environment.check_for_reparse
102+
validation_environment.known_resource_types.clear
103+
ensure
104+
Puppet[:tasks] = original_taskmode if Puppet.tasks_supported?
105+
end
106+
end
94107
rescue StandardError => detail
95108
# Sometimes the error is in the cause not the root object itself
96109
detail = detail.cause if !detail.respond_to?(:line) && detail.respond_to?(:cause)

lib/puppet-languageserver/message_router.rb

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def receive_request(request)
9797
begin
9898
case documents.document_type(file_uri)
9999
when :manifest
100-
request.reply_result(PuppetLanguageServer::Manifest::CompletionProvider.complete(content, line_num, char_num))
100+
request.reply_result(PuppetLanguageServer::Manifest::CompletionProvider.complete(content, line_num, char_num, :tasks_mode => PuppetLanguageServer::DocumentStore.module_plan_file?(file_uri)))
101101
else
102102
raise "Unable to provide completion on #{file_uri}"
103103
end
@@ -123,7 +123,7 @@ def receive_request(request)
123123
begin
124124
case documents.document_type(file_uri)
125125
when :manifest
126-
request.reply_result(PuppetLanguageServer::Manifest::HoverProvider.resolve(content, line_num, char_num))
126+
request.reply_result(PuppetLanguageServer::Manifest::HoverProvider.resolve(content, line_num, char_num, :tasks_mode => PuppetLanguageServer::DocumentStore.module_plan_file?(file_uri)))
127127
else
128128
raise "Unable to provide hover on #{file_uri}"
129129
end
@@ -140,7 +140,7 @@ def receive_request(request)
140140
begin
141141
case documents.document_type(file_uri)
142142
when :manifest
143-
request.reply_result(PuppetLanguageServer::Manifest::DefinitionProvider.find_definition(content, line_num, char_num))
143+
request.reply_result(PuppetLanguageServer::Manifest::DefinitionProvider.find_definition(content, line_num, char_num, :tasks_mode => PuppetLanguageServer::DocumentStore.module_plan_file?(file_uri)))
144144
else
145145
raise "Unable to provide definition on #{file_uri}"
146146
end
@@ -155,8 +155,7 @@ def receive_request(request)
155155
begin
156156
case documents.document_type(file_uri)
157157
when :manifest
158-
result = PuppetLanguageServer::Manifest::DocumentSymbolProvider.extract_document_symbols(content)
159-
request.reply_result(result)
158+
request.reply_result(PuppetLanguageServer::Manifest::DocumentSymbolProvider.extract_document_symbols(content, :tasks_mode => PuppetLanguageServer::DocumentStore.module_plan_file?(file_uri)))
160159
else
161160
raise "Unable to provide definition on #{file_uri}"
162161
end

lib/puppet-languageserver/puppet_monkey_patches.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
1+
# Monkey Patch the Puppet language parser so we can globally lock any changes to the
2+
# global setting Puppet[:tasks]. We need to manage this so we can switch between
3+
# parsing modes. Unfortunately we can't do this as method parameter, only via the
4+
# global Puppet settings which is not thread safe
5+
$PuppetParserMutex = Mutex.new # rubocop:disable Style/GlobalVars
6+
module Puppet
7+
module Pops
8+
module Parser
9+
class Parser
10+
def singleton_parse_string(code, task_mode = false, path = nil)
11+
$PuppetParserMutex.synchronize do # rubocop:disable Style/GlobalVars
12+
begin
13+
original_taskmode = Puppet[:tasks] if Puppet.tasks_supported?
14+
Puppet[:tasks] = task_mode if Puppet.tasks_supported?
15+
return parse_string(code, path)
16+
ensure
17+
Puppet[:tasks] = original_taskmode if Puppet.tasks_supported?
18+
end
19+
end
20+
end
21+
end
22+
end
23+
end
24+
end
25+
26+
module Puppet
27+
# Tasks first appeared in Puppet 5.4.0
28+
def self.tasks_supported?
29+
Gem::Version.new(Puppet.version) >= Gem::Version.new('5.4.0')
30+
end
31+
end
32+
133
# MUST BE LAST!!!!!!
234
# Suppress any warning messages to STDOUT. It can pollute stdout when running in STDIO mode
335
Puppet::Util::Log.newdesttype :null_logger do

lib/puppet-languageserver/puppet_parser_helper.rb

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,19 @@ def self.get_line_at(content, line_offsets, line_num)
5656
end
5757
end
5858

59-
def self.object_under_cursor(content, line_num, char_num, multiple_attempts = false, disallowed_classes = [])
59+
def self.object_under_cursor(content, line_num, char_num, options)
60+
options = {
61+
:multiple_attempts => false,
62+
:disallowed_classes => [],
63+
:tasks_mode => false
64+
}.merge(options)
65+
6066
# Use Puppet to generate the AST
6167
parser = Puppet::Pops::Parser::Parser.new
6268

6369
# Calculating the line offsets can be expensive and is only required
6470
# if we're doing mulitple passes of parsing
65-
line_offsets = line_offsets(content) if multiple_attempts
71+
line_offsets = line_offsets(content) if options[:multiple_attempts]
6672

6773
result = nil
6874
move_offset = 0
@@ -108,10 +114,10 @@ def self.object_under_cursor(content, line_num, char_num, multiple_attempts = fa
108114
next if new_content.nil?
109115

110116
begin
111-
result = parser.parse_string(new_content, '')
117+
result = parser.singleton_parse_string(new_content, options[:tasks_mode], '')
112118
break
113119
rescue Puppet::ParseErrorWithIssue => _exception
114-
next if multiple_attempts
120+
next if options[:multiple_attempts]
115121
raise
116122
end
117123
end
@@ -140,14 +146,14 @@ def self.object_under_cursor(content, line_num, char_num, multiple_attempts = fa
140146
valid_models = []
141147
if result.model.respond_to? :eAllContents
142148
valid_models = result.model.eAllContents.select do |item|
143-
check_for_valid_item(item, abs_offset, disallowed_classes)
149+
check_for_valid_item(item, abs_offset, options[:disallowed_classes])
144150
end
145151

146152
valid_models.sort! { |a, b| a.length - b.length }
147153
else
148154
path = []
149155
result.model._pcore_all_contents(path) do |item|
150-
if check_for_valid_item(item, abs_offset, disallowed_classes) # rubocop:disable Style/IfUnlessModifier Nicer to read like this
156+
if check_for_valid_item(item, abs_offset, options[:disallowed_classes]) # rubocop:disable Style/IfUnlessModifier Nicer to read like this
151157
valid_models.push(model_path_struct.new(item, path.dup))
152158
end
153159
end
Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,35 @@
1+
require 'uri'
2+
require 'puppet'
3+
14
module PuppetLanguageServer
25
module UriHelper
36
def self.build_file_uri(path)
4-
path.start_with?('/') ? 'file://' + path : 'file:///' + path
7+
'file://' + Puppet::Util.uri_encode(path.start_with?('/') ? path : '/' + path)
8+
end
9+
10+
# Compares two URIs and returns the relative path
11+
#
12+
# @param root_uri [String] The root URI to compare to
13+
# @param uri [String] The URI to compare to the root
14+
# @param case_sensitive [Boolean] Whether the path comparison is case senstive or not. Default is true
15+
# @return [String] Returns the relative path string if the URI is indeed a child of the root, otherwise returns nil
16+
def self.relative_uri_path(root_uri, uri, case_sensitive = true)
17+
actual_root = URI(root_uri)
18+
actual_uri = URI(uri)
19+
return nil unless actual_root.scheme == actual_uri.scheme
20+
21+
# CGI.unescape doesn't handle space rules properly in uri paths
22+
# URI.unescape does, but returns strings in their original encoding
23+
# Mostly safe here as we're only worried about file based URIs
24+
root_path = URI.unescape(actual_root.path) # rubocop:disable Lint/UriEscapeUnescape
25+
uri_path = URI.unescape(actual_uri.path) # rubocop:disable Lint/UriEscapeUnescape
26+
if case_sensitive
27+
return nil unless uri_path.slice(0, root_path.length) == root_path
28+
else
29+
return nil unless uri_path.slice(0, root_path.length).casecmp(root_path).zero?
30+
end
31+
32+
uri_path.slice(root_path.length..-1)
533
end
634
end
735
end

0 commit comments

Comments
 (0)