puppet-editor-services 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +510 -0
  3. data/CODEOWNERS +2 -0
  4. data/CODE_OF_CONDUCT.md +46 -0
  5. data/CONTRIBUTING.md +54 -0
  6. data/Gemfile +53 -0
  7. data/LICENSE +201 -0
  8. data/README.md +308 -0
  9. data/Rakefile +185 -0
  10. data/bin/puppet-debugserver +8 -0
  11. data/bin/puppet-languageserver +7 -0
  12. data/bin/puppet-languageserver-sidecar +7 -0
  13. data/lib/dsp/dsp.rb +7 -0
  14. data/lib/dsp/dsp_base.rb +62 -0
  15. data/lib/dsp/dsp_protocol.rb +4619 -0
  16. data/lib/lsp/lsp.rb +10 -0
  17. data/lib/lsp/lsp_base.rb +63 -0
  18. data/lib/lsp/lsp_custom.rb +170 -0
  19. data/lib/lsp/lsp_enums.rb +143 -0
  20. data/lib/lsp/lsp_protocol.rb +2785 -0
  21. data/lib/lsp/lsp_protocol_callhierarchy.proposed.rb +239 -0
  22. data/lib/lsp/lsp_protocol_colorprovider.rb +100 -0
  23. data/lib/lsp/lsp_protocol_configuration.rb +82 -0
  24. data/lib/lsp/lsp_protocol_declaration.rb +73 -0
  25. data/lib/lsp/lsp_protocol_foldingrange.rb +129 -0
  26. data/lib/lsp/lsp_protocol_implementation.rb +75 -0
  27. data/lib/lsp/lsp_protocol_progress.rb +200 -0
  28. data/lib/lsp/lsp_protocol_selectionrange.rb +79 -0
  29. data/lib/lsp/lsp_protocol_sematictokens.proposed.rb +340 -0
  30. data/lib/lsp/lsp_protocol_typedefinition.rb +75 -0
  31. data/lib/lsp/lsp_protocol_workspacefolders.rb +174 -0
  32. data/lib/lsp/lsp_types.rb +1534 -0
  33. data/lib/puppet-debugserver/debug_session/break_points.rb +137 -0
  34. data/lib/puppet-debugserver/debug_session/flow_control.rb +161 -0
  35. data/lib/puppet-debugserver/debug_session/hook_handlers.rb +295 -0
  36. data/lib/puppet-debugserver/debug_session/puppet_session_run_mode.rb +66 -0
  37. data/lib/puppet-debugserver/debug_session/puppet_session_state.rb +122 -0
  38. data/lib/puppet-debugserver/hooks.rb +132 -0
  39. data/lib/puppet-debugserver/message_handler.rb +277 -0
  40. data/lib/puppet-debugserver/puppet_debug_session.rb +541 -0
  41. data/lib/puppet-debugserver/puppet_monkey_patches.rb +118 -0
  42. data/lib/puppet-languageserver/client_session_state.rb +119 -0
  43. data/lib/puppet-languageserver/crash_dump.rb +50 -0
  44. data/lib/puppet-languageserver/epp/validation_provider.rb +34 -0
  45. data/lib/puppet-languageserver/facter_helper.rb +25 -0
  46. data/lib/puppet-languageserver/global_queues/sidecar_queue.rb +205 -0
  47. data/lib/puppet-languageserver/global_queues/single_instance_queue.rb +126 -0
  48. data/lib/puppet-languageserver/global_queues/validation_queue.rb +102 -0
  49. data/lib/puppet-languageserver/global_queues.rb +16 -0
  50. data/lib/puppet-languageserver/manifest/completion_provider.rb +331 -0
  51. data/lib/puppet-languageserver/manifest/definition_provider.rb +99 -0
  52. data/lib/puppet-languageserver/manifest/document_symbol_provider.rb +228 -0
  53. data/lib/puppet-languageserver/manifest/folding_provider.rb +226 -0
  54. data/lib/puppet-languageserver/manifest/format_on_type_provider.rb +143 -0
  55. data/lib/puppet-languageserver/manifest/hover_provider.rb +221 -0
  56. data/lib/puppet-languageserver/manifest/signature_provider.rb +169 -0
  57. data/lib/puppet-languageserver/manifest/validation_provider.rb +127 -0
  58. data/lib/puppet-languageserver/message_handler.rb +462 -0
  59. data/lib/puppet-languageserver/providers.rb +18 -0
  60. data/lib/puppet-languageserver/puppet_helper.rb +108 -0
  61. data/lib/puppet-languageserver/puppet_lexer_helper.rb +55 -0
  62. data/lib/puppet-languageserver/puppet_monkey_patches.rb +39 -0
  63. data/lib/puppet-languageserver/puppet_parser_helper.rb +212 -0
  64. data/lib/puppet-languageserver/puppetfile/validation_provider.rb +185 -0
  65. data/lib/puppet-languageserver/server_capabilities.rb +48 -0
  66. data/lib/puppet-languageserver/session_state/document_store.rb +272 -0
  67. data/lib/puppet-languageserver/session_state/language_client.rb +239 -0
  68. data/lib/puppet-languageserver/session_state/object_cache.rb +162 -0
  69. data/lib/puppet-languageserver/sidecar_protocol.rb +532 -0
  70. data/lib/puppet-languageserver/uri_helper.rb +46 -0
  71. data/lib/puppet-languageserver-sidecar/cache/base.rb +36 -0
  72. data/lib/puppet-languageserver-sidecar/cache/filesystem.rb +111 -0
  73. data/lib/puppet-languageserver-sidecar/cache/null.rb +27 -0
  74. data/lib/puppet-languageserver-sidecar/facter_helper.rb +41 -0
  75. data/lib/puppet-languageserver-sidecar/puppet_environment_monkey_patches.rb +52 -0
  76. data/lib/puppet-languageserver-sidecar/puppet_helper.rb +281 -0
  77. data/lib/puppet-languageserver-sidecar/puppet_modulepath_monkey_patches.rb +146 -0
  78. data/lib/puppet-languageserver-sidecar/puppet_monkey_patches.rb +9 -0
  79. data/lib/puppet-languageserver-sidecar/puppet_parser_helper.rb +77 -0
  80. data/lib/puppet-languageserver-sidecar/puppet_strings_helper.rb +399 -0
  81. data/lib/puppet-languageserver-sidecar/puppet_strings_monkey_patches.rb +16 -0
  82. data/lib/puppet-languageserver-sidecar/sidecar_protocol_extensions.rb +16 -0
  83. data/lib/puppet-languageserver-sidecar/workspace.rb +89 -0
  84. data/lib/puppet_debugserver.rb +164 -0
  85. data/lib/puppet_editor_services/connection/base.rb +62 -0
  86. data/lib/puppet_editor_services/connection/stdio.rb +25 -0
  87. data/lib/puppet_editor_services/connection/tcp.rb +34 -0
  88. data/lib/puppet_editor_services/handler/base.rb +16 -0
  89. data/lib/puppet_editor_services/handler/debug_adapter.rb +63 -0
  90. data/lib/puppet_editor_services/handler/json_rpc.rb +133 -0
  91. data/lib/puppet_editor_services/logging.rb +45 -0
  92. data/lib/puppet_editor_services/protocol/base.rb +27 -0
  93. data/lib/puppet_editor_services/protocol/debug_adapter.rb +135 -0
  94. data/lib/puppet_editor_services/protocol/debug_adapter_messages.rb +171 -0
  95. data/lib/puppet_editor_services/protocol/json_rpc.rb +241 -0
  96. data/lib/puppet_editor_services/protocol/json_rpc_messages.rb +200 -0
  97. data/lib/puppet_editor_services/server/base.rb +42 -0
  98. data/lib/puppet_editor_services/server/stdio.rb +85 -0
  99. data/lib/puppet_editor_services/server/tcp.rb +349 -0
  100. data/lib/puppet_editor_services/server.rb +15 -0
  101. data/lib/puppet_editor_services/version.rb +36 -0
  102. data/lib/puppet_editor_services.rb +8 -0
  103. data/lib/puppet_languageserver.rb +263 -0
  104. data/lib/puppet_languageserver_sidecar.rb +361 -0
  105. data/puppet-debugserver +11 -0
  106. data/puppet-editor-services.gemspec +29 -0
  107. data/puppet-languageserver +15 -0
  108. data/puppet-languageserver-sidecar +14 -0
  109. metadata +240 -0
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetLanguageServerSidecar
4
+ module Cache
5
+ class FileSystem < Base
6
+ attr_reader :cache_dir
7
+
8
+ def initialize(_options = {})
9
+ super
10
+ require 'digest'
11
+ require 'json'
12
+ require 'tmpdir'
13
+
14
+ @cache_dir = File.join(Dir.tmpdir, 'puppet-vscode-cache')
15
+ begin
16
+ Dir.mkdir(@cache_dir) unless Dir.exist?(@cache_dir)
17
+ rescue Errno::ENOENT => e
18
+ PuppetLanguageServerSidecar.log_message(:error, "[PuppetLanguageServerSidecar::Cache::FileSystem] An error occured while creating file cache. Disabling cache: #{e}")
19
+ @cache_dir = nil
20
+ end
21
+ end
22
+
23
+ def active?
24
+ !@cache_dir.nil?
25
+ end
26
+
27
+ def load(absolute_path, section)
28
+ return nil unless active?
29
+
30
+ file_key = file_key(absolute_path, section)
31
+ cache_file = File.join(cache_dir, cache_filename(file_key))
32
+
33
+ content = read_file(cache_file)
34
+ return nil if content.nil?
35
+
36
+ json_obj = JSON.parse(content)
37
+ return nil if json_obj.nil?
38
+
39
+ # Check that this is from the same language server version
40
+ unless json_obj['sidecar_version'] == PuppetLanguageServerSidecar.version
41
+ PuppetLanguageServerSidecar.log_message(:debug, "[PuppetLanguageServerSidecar::Cache::FileSystem.load] Error loading #{absolute_path}: Expected sidecar_version version #{PuppetLanguageServerSidecar.version} but found #{json_obj['sidecar_version']}")
42
+ return nil
43
+ end
44
+ # Check that the source file hash matches
45
+ content_hash = calculate_hash(absolute_path)
46
+ if json_obj['file_hash'] != content_hash
47
+ PuppetLanguageServerSidecar.log_message(:debug, "[PuppetLanguageServerSidecar::Cache::FileSystem.load] Error loading #{absolute_path}: Expected file_hash of #{content_hash} but found #{json_obj['file_hash']}")
48
+ return nil
49
+ end
50
+ PuppetLanguageServerSidecar.log_message(:debug, "[PuppetLanguageServerSidecar::Cache::FileSystem.load] Loading #{absolute_path} from cache")
51
+
52
+ json_obj['data']
53
+ rescue RuntimeError => e
54
+ PuppetLanguageServerSidecar.log_message(:debug, "[PuppetLanguageServerSidecar::Cache::FileSystem.load] Error loading #{absolute_path}: #{e}")
55
+ raise
56
+ end
57
+
58
+ def save(absolute_path, section, content_string)
59
+ return false unless active?
60
+
61
+ file_key = file_key(absolute_path, section)
62
+ cache_file = File.join(cache_dir, cache_filename(file_key))
63
+
64
+ content = { 'data' => content_string }
65
+ # Inject metadata
66
+ content['sidecar_version'] = PuppetLanguageServerSidecar.version
67
+ content['file_hash'] = calculate_hash(absolute_path)
68
+ content['created'] = Time.now.utc.strftime('%FT%T')
69
+ content['path'] = absolute_path
70
+ content['section'] = section
71
+
72
+ PuppetLanguageServerSidecar.log_message(:debug, "[PuppetLanguageServerSidecar::Cache::FileSystem.save] Saving #{absolute_path} to cache")
73
+ save_file(cache_file, content.to_json)
74
+ end
75
+
76
+ def clear!
77
+ return unless active?
78
+
79
+ PuppetLanguageServerSidecar.log_message(:warn, '[PuppetLanguageServerSidecar::Cache::FileSystem.clear] Filesystem based cache is being cleared')
80
+ FileUtils.rm(Dir.glob(File.join(cache_dir, '*')), force: true)
81
+ end
82
+
83
+ private
84
+
85
+ def file_key(filepath, section)
86
+ # Strictly speaking some file systems are case sensitive but ruby/puppet throws a fit
87
+ # with naming if you do
88
+ filepath.downcase + File::SEPARATOR + section
89
+ end
90
+
91
+ def read_file(filepath)
92
+ return nil unless File.exist?(filepath)
93
+
94
+ File.binread(filepath)
95
+ end
96
+
97
+ def save_file(filepath, content)
98
+ File.binwrite(filepath, content)
99
+ true
100
+ end
101
+
102
+ def cache_filename(file_key)
103
+ "#{Digest::SHA256.hexdigest(file_key)}.txt"
104
+ end
105
+
106
+ def calculate_hash(filepath)
107
+ Digest::SHA256.hexdigest(read_file(filepath))
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetLanguageServerSidecar
4
+ module Cache
5
+ class Null < Base
6
+ def initialize(options = {})
7
+ super
8
+ end
9
+
10
+ def active?
11
+ false
12
+ end
13
+
14
+ def load(*)
15
+ nil
16
+ end
17
+
18
+ def save(*)
19
+ true
20
+ end
21
+
22
+ def clear!
23
+ nil
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PuppetLanguageServerSidecar
4
+ module FacterHelper
5
+ def self.current_environment
6
+ begin
7
+ env = Puppet.lookup(:environments).get!(Puppet.settings[:environment])
8
+ return env unless env.nil?
9
+ rescue Puppet::Environments::EnvironmentNotFound
10
+ PuppetLanguageServerSidecar.log_message(:warning, "[FacterHelper::current_environment] Unable to load environment #{Puppet.settings[:environment]}")
11
+ rescue StandardError => e
12
+ PuppetLanguageServerSidecar.log_message(:warning, "[FacterHelper::current_environment] Error loading environment #{Puppet.settings[:environment]}: #{e}")
13
+ end
14
+ Puppet.lookup(:current_environment)
15
+ end
16
+
17
+ def self.retrieve_facts(_cache, _options = {})
18
+ require 'puppet/indirector/facts/facter'
19
+
20
+ PuppetLanguageServerSidecar.log_message(:debug, '[FacterHelper::retrieve_facts] Starting')
21
+ facts = PuppetLanguageServer::Sidecar::Protocol::FactList.new
22
+ begin
23
+ req = Puppet::Indirector::Request.new(:facts, :find, 'language_server', nil, environment: current_environment)
24
+ result = Puppet::Node::Facts::Facter.new.find(req)
25
+ result.values.each do |key, value|
26
+ # TODO: This isn't strictly correct e.g. fully qualified facts will look a bit odd.
27
+ # Consider a fact called foo.bar.baz = 'Hello'. Even though the fact name is `foo.bar.baz`
28
+ # it will appear in the facts object as `facts['foo'] = { 'bar' => { 'baz' => 'Hello' }}`
29
+ facts << PuppetLanguageServer::Sidecar::Protocol::Fact.new.from_h!('key' => key, 'value' => value)
30
+ end
31
+ rescue StandardError => e
32
+ PuppetLanguageServerSidecar.log_message(:error, "[FacterHelper::_load_facts] Error loading facts #{e.message} #{e.backtrace}")
33
+ rescue LoadError => e
34
+ PuppetLanguageServerSidecar.log_message(:error, "[FacterHelper::_load_facts] Error loading facts (LoadError) #{e.message} #{e.backtrace}")
35
+ end
36
+
37
+ PuppetLanguageServerSidecar.log_message(:debug, "[FacterHelper::retrieve_facts] Finished loading #{facts.count} facts")
38
+ facts
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puppet::Environments
4
+ class Directories
5
+ # Monkey patch the environment loader. When it attempts to load the special sidecar
6
+ # environment, create a new Puppet::Node::Environment object for the workspace
7
+ alias original_get get
8
+ def get(name)
9
+ if name.intern == PuppetLanguageServerSidecar::PuppetHelper::SIDECAR_PUPPET_ENVIRONMENT.intern
10
+ env_symbol = name.intern
11
+ setting_values = Puppet.settings.values(env_symbol, Puppet.settings.preferred_run_mode)
12
+ env = Puppet::Node::Environment.create(
13
+ env_symbol,
14
+ Puppet::Node::Environment.split_path(setting_values.interpolate(:modulepath)),
15
+ setting_values.interpolate(:manifest),
16
+ setting_values.interpolate(:config_version)
17
+ )
18
+ return env
19
+ end
20
+
21
+ original_get(name)
22
+ end
23
+
24
+ # Monkey patch the environment loader. When it attempts to load the special sidecar
25
+ # environment.conf file, create a new Puppet::Settings::EnvironmentConf object
26
+ # from the workspace.
27
+ alias original_get_conf get_conf
28
+ def get_conf(name)
29
+ if name.intern == PuppetLanguageServerSidecar::PuppetHelper::SIDECAR_PUPPET_ENVIRONMENT.intern
30
+ conf = Puppet::Settings::EnvironmentConf.load_from(PuppetLanguageServerSidecar::Workspace.root_path, @global_module_path)
31
+ # Unfortunately the environment.conf expects OS style delimiters which means
32
+ # it fails if written for windows and read on Unix and vice versa. So we just
33
+ # inline munge the modulepath
34
+ modpath = conf.get_raw_setting(:modulepath).value
35
+ modpath.gsub!(':', File::PATH_SEPARATOR) if File::PATH_SEPARATOR != ':'
36
+ modpath.gsub!(';', File::PATH_SEPARATOR) if File::PATH_SEPARATOR != ';'
37
+ return conf
38
+ end
39
+
40
+ original_get_conf(name)
41
+ end
42
+ end
43
+ end
44
+
45
+ # Monkey patch the environment. Normally it's not possible to modify environment settings.
46
+ # Add a method to get the underlying environment settings which can be used to modify
47
+ # settings on the fly.
48
+ class Puppet::Settings::EnvironmentConf
49
+ def get_raw_setting(setting_name)
50
+ section.setting(setting_name) if section
51
+ end
52
+ end
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppet/indirector/face'
4
+
5
+ module PuppetLanguageServerSidecar
6
+ module PuppetHelper
7
+ SIDECAR_PUPPET_ENVIRONMENT = 'sidecarenvironment'
8
+
9
+ # Ignore certain data types. For more information see https://tickets.puppetlabs.com/browse/DOCUMENT-1020
10
+ # TypeReference - Internal to the Puppet Data Type system
11
+ # TypeAlias - Internal to the Puppet Data Type system
12
+ # Object - While useful, typically only needed when extended the type system as opposed to general use
13
+ # TypeSet - While useful, typically only needed when extended the type system as opposed to general use
14
+ IGNORE_DATATYPE_NAMES = %w[TypeReference TypeAlias Object TypeSet ObjectTypeExten Iterable AbstractTimeData TypeWithContained].freeze
15
+
16
+ # Resource Face
17
+ def self.get_puppet_resource(typename, title = nil)
18
+ result = PuppetLanguageServer::Sidecar::Protocol::PuppetClassList.new
19
+ resources = if title.nil?
20
+ Puppet::Face[:resource, '0.0.1'].search(typename)
21
+ else
22
+ Puppet::Face[:resource, '0.0.1'].find("#{typename}/#{title}")
23
+ end
24
+ return result if resources.nil?
25
+
26
+ resources = [resources] unless resources.is_a?(Array)
27
+ prune_resource_parameters(resources).each do |item|
28
+ obj = PuppetLanguageServer::Sidecar::Protocol::Resource.new
29
+ obj.manifest = item.to_manifest
30
+ result << obj
31
+ end
32
+ result
33
+ end
34
+
35
+ # Puppet Strings loading
36
+ def self.available_documentation_types
37
+ %I[class datatype function type]
38
+ end
39
+
40
+ def self.retrieve_default_data_types
41
+ default_types = PuppetLanguageServer::Sidecar::Protocol::PuppetDataTypeList.new
42
+ # There's no actual record of all available types, but we can use the Puppet::Pops::Types to implicitly get their names
43
+ name_list = []
44
+
45
+ Puppet::Pops::Types.constants.each do |const|
46
+ thing = Puppet::Pops::Types.const_get(const)
47
+ # Not that this is a reference to a type, not an INSTANCE of that type.
48
+ # So we can't do .is_a? checks. But instead look for methods on the thing
49
+ # that would indicate it's a Puppet Type
50
+ # _pcore_type : is present on all Pops type objects
51
+ # simple_name : comes from PAnyType
52
+ next unless thing.respond_to?(:_pcore_type) && thing.respond_to?(:simple_name)
53
+ # Ignore certain data types
54
+ next if IGNORE_DATATYPE_NAMES.include?(thing.simple_name)
55
+ # Don't need duplicates
56
+ next if name_list.include?(thing.simple_name)
57
+
58
+ name_list << thing.simple_name
59
+
60
+ obj = PuppetLanguageServer::Sidecar::Protocol::PuppetDataType.new
61
+ obj.key = thing.simple_name
62
+ obj.source = nil
63
+ obj.calling_source = nil
64
+ obj.line = nil
65
+ obj.doc = "The #{thing.simple_name} core data type"
66
+ obj.is_type_alias = false
67
+ obj.alias_of = nil
68
+ # So far, no core data types have attributes
69
+ obj.attributes = []
70
+ default_types << obj
71
+ end
72
+
73
+ default_types
74
+ end
75
+ private_class_method :retrieve_default_data_types
76
+
77
+ # Retrieve objects via the Puppet 4 API loaders
78
+ def self.retrieve_via_puppet_strings(cache, options = {})
79
+ PuppetLanguageServerSidecar.log_message(:debug, '[PuppetHelper::retrieve_via_puppet_strings] Starting')
80
+
81
+ object_types = options[:object_types].nil? ? available_documentation_types : options[:object_types]
82
+ object_types.select! { |i| available_documentation_types.include?(i) }
83
+
84
+ result = PuppetLanguageServer::Sidecar::Protocol::AggregateMetadata.new
85
+ return result if object_types.empty?
86
+
87
+ current_env = current_environment
88
+ for_agent = options[:for_agent].nil? ? true : options[:for_agent]
89
+ Puppet::Pops::Loaders.new(current_env, for_agent)
90
+
91
+ finder = PuppetPathFinder.new(current_env, object_types)
92
+ paths = finder.find(options[:root_path])
93
+
94
+ paths.each do |path|
95
+ file_doc = PuppetLanguageServerSidecar::PuppetStringsHelper.file_documentation(path, finder.puppet_path, cache)
96
+ next if file_doc.nil?
97
+
98
+ if object_types.include?(:class) # rubocop:disable Style/IfUnlessModifier This reads better
99
+ file_doc.classes.each { |item| result.append!(item) }
100
+ end
101
+ if object_types.include?(:datatype) # rubocop:disable Style/IfUnlessModifier This reads better
102
+ file_doc.datatypes.each { |item| result.append!(item) }
103
+ end
104
+ if object_types.include?(:function) # rubocop:disable Style/IfUnlessModifier This reads better
105
+ file_doc.functions.each { |item| result.append!(item) }
106
+ end
107
+ next unless object_types.include?(:type)
108
+
109
+ file_doc.types.each do |item|
110
+ result.append!(item) unless name == 'whit' || name == 'component'
111
+ finder.temp_file.unlink if item.key == 'file' && File.exist?(finder.temp_file.path) # Remove the temp_file.rb if it exists
112
+ end
113
+ end
114
+
115
+ # Remove Puppet3 functions which have a Puppet4 function already loaded
116
+ if object_types.include?(:function) && !result.functions.nil?
117
+ pup4_functions = result.functions.select { |i| i.function_version == 4 }.map { |i| i.key }
118
+ result.functions.reject! { |i| i.function_version == 3 && pup4_functions.include?(i.key) }
119
+ end
120
+
121
+ # Add the inbuilt data types if there's no root path
122
+ result.concat!(retrieve_default_data_types) if object_types.include?(:datatype) && options[:root_path].nil?
123
+
124
+ result.each_list { |key, item| PuppetLanguageServerSidecar.log_message(:debug, "[PuppetHelper::retrieve_via_puppet_strings] Finished loading #{item.count} #{key}") }
125
+ result
126
+ end
127
+
128
+ # Private functions
129
+
130
+ def self.prune_resource_parameters(resources)
131
+ # From https://github.com/puppetlabs/puppet/blob/488661d84e54904124514ab9e4500e81b10f84d1/lib/puppet/application/resource.rb#L146-L148
132
+ if resources.is_a?(Array)
133
+ resources.map(&:prune_parameters)
134
+ else
135
+ resources.prune_parameters
136
+ end
137
+ end
138
+ private_class_method :prune_resource_parameters
139
+
140
+ def self.current_environment
141
+ begin
142
+ env = Puppet.lookup(:environments).get!(Puppet.settings[:environment])
143
+ return env unless env.nil?
144
+ rescue Puppet::Environments::EnvironmentNotFound
145
+ PuppetLanguageServerSidecar.log_message(:warning, "[PuppetHelper::current_environment] Unable to load environment #{Puppet.settings[:environment]}")
146
+ rescue StandardError => e
147
+ PuppetLanguageServerSidecar.log_message(:warning, "[PuppetHelper::current_environment] Error loading environment #{Puppet.settings[:environment]}: #{e}")
148
+ end
149
+ Puppet.lookup(:current_environment)
150
+ end
151
+ private_class_method :current_environment
152
+
153
+ # A helper class to find the paths for different kinds of things related to Puppet, for example
154
+ # DataType ruby files or manifests.
155
+ class PuppetPathFinder
156
+ attr_reader :object_types, :puppet_path, :temp_file
157
+
158
+ # @param puppet_env [Puppet::Node::Environment] The environment to search within
159
+ # @param object_types [Symbol] The types of objects that will be searched for. See available_documentation_types for the complete list
160
+ def initialize(puppet_env, object_types)
161
+ # Path to every module
162
+ @module_paths = puppet_env.modules.map(&:path)
163
+ # Path to the environment
164
+ @env_path = puppet_env.configuration.path_to_env
165
+ # Path to the puppet installation
166
+ @puppet_path = if puppet_env.loaders.nil? # No loaders have been created yet
167
+ nil
168
+ elsif puppet_env.loaders.puppet_system_loader.nil?
169
+ nil
170
+ elsif puppet_env.loaders.puppet_system_loader.lib_root?
171
+ File.join(puppet_env.loaders.puppet_system_loader.path, '..')
172
+ else
173
+ puppet_env.loaders.puppet_system_loader.path
174
+ end
175
+ # Path to the cached puppet files e.g. pluginsync
176
+ @vardir_path = Puppet.settings[:vardir]
177
+
178
+ @object_types = object_types
179
+ end
180
+
181
+ # Find all puppet related files, optionally from within a root path
182
+ # @param from_root_path [String] The path which files can be found within. If nil, only the default Puppet locations are searched e.g. vardir
183
+ # @return [Array[String]] A list of all files that are found. This is the absolute path to the file.
184
+ def find(from_root_path = nil)
185
+ require 'tempfile'
186
+ paths = []
187
+ search_paths = @module_paths.nil? ? [] : @module_paths
188
+ search_paths << @env_path unless @env_path.nil?
189
+ search_paths << @vardir_path unless @vardir_path.nil?
190
+ search_paths << @puppet_path unless @puppet_path.nil?
191
+
192
+ searched_globs = []
193
+ search_paths.each do |search_root|
194
+ PuppetLanguageServerSidecar.log_message(:debug, "[PuppetPathFinder] Potential search root '#{search_root}'")
195
+ next if search_root.nil?
196
+
197
+ # We need absolute paths from here on in.
198
+ search_root = File.expand_path(search_root)
199
+ next unless path_in_root?(from_root_path, search_root) && Dir.exist?(search_root)
200
+
201
+ PuppetLanguageServerSidecar.log_message(:debug, "[PuppetPathFinder] Using '#{search_root}' as a directory to search")
202
+ # name of temp file to store the file type definitions (if any)
203
+ @temp_file = Tempfile.new('file.rb')
204
+ all_object_info.each do |object_type, paths_to_search|
205
+ next unless object_types.include?(object_type)
206
+
207
+ # TODO: next unless object_type is included
208
+ paths_to_search.each do |path_info|
209
+ path = File.join(search_root, path_info[:relative_dir])
210
+ glob = path + path_info[:glob]
211
+ next if searched_globs.include?(glob) # No point searching twice
212
+ next unless Dir.exist?(path)
213
+
214
+ searched_globs << glob
215
+ PuppetLanguageServerSidecar.log_message(:debug, "[PuppetPathFinder] Searching glob '#{glob}''")
216
+
217
+ Dir.glob(glob) do |filename|
218
+ # if filename matches file.rb or /file/<any>.rb then we need to loop through each file type definition
219
+ if filename.match?(%r{/type/file/.*.rb|/type/file.rb})
220
+ PuppetLanguageServerSidecar.log_message(:debug, "[PuppetPathFinder] Found file type definition at '#{filename}'.")
221
+ # Read each file type definition and write it to the temp file
222
+ @temp_file.write(File.read(filename))
223
+ else
224
+ paths << filename
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
230
+ #  Add the temp_file.rb to the paths array for searching (if exists)
231
+ if @temp_file && File.exist?(@temp_file.path)
232
+ paths << @temp_file.path
233
+ @temp_file.close
234
+ end
235
+ paths
236
+ end
237
+
238
+ private
239
+
240
+ # Simple text based path checking
241
+ # Is [path] in the [root]
242
+ # @param root [String] The Root path
243
+ # @param path [String] The path to check if it's within the root
244
+ # @return [Boolean]
245
+ def path_in_root?(root, path)
246
+ # Doesn't matter what the root is, if the path is nil, it's false
247
+ return false if path.nil?
248
+ # Doesn't matter what the path is, if the root is nil it's true.
249
+ return true if root.nil?
250
+ # If the path is less than root, then it has to be false
251
+ return false if root.length > path.length
252
+
253
+ # Is the beginning of the path, the same as the root
254
+ value = path.slice(0, root.length)
255
+ value.casecmp(root).zero?
256
+ end
257
+
258
+ # The metadata for all object types and where they can be found on the filesystem
259
+ # @return [Hash[Symbol => Hash[Symbol => String]]]
260
+ def all_object_info
261
+ {
262
+ class: [
263
+ { relative_dir: 'manifests', glob: '/**/*.pp' } # Pretty much everything in most modules
264
+ ],
265
+ datatype: [
266
+ { relative_dir: 'lib/puppet/datatypes', glob: '/**/*.rb' }, # Custom Data Types
267
+ { relative_dir: 'types', glob: '/**/*.pp' } # Data Type aliases
268
+ ],
269
+ function: [
270
+ { relative_dir: 'functions', glob: '/**/*.pp' }, # Contains custom functions written in the Puppet language.
271
+ { relative_dir: 'lib/puppet/functions', glob: '/**/*.rb' }, # Contains functions written in Ruby for the modern Puppet::Functions API
272
+ { relative_dir: 'lib/puppet/parser/functions', glob: '/**/*.rb' } # Contains functions written in Ruby for the legacy Puppet::Parser::Functions API
273
+ ],
274
+ type: [
275
+ { relative_dir: 'lib/puppet/type', glob: '/{,file/}*.rb' } # Contains Puppet resource types. Resource types like `file` can live in subdirs, hence the glob
276
+ ]
277
+ }
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Inject the workspace libdir on the fly
4
+ require 'puppet/util/autoload'
5
+ module Puppet
6
+ module Util
7
+ class Autoload
8
+ class << self
9
+ alias original_module_directories module_directories
10
+ def module_directories(env)
11
+ result = original_module_directories(env)
12
+ return result unless PuppetLanguageServerSidecar::Workspace.has_module_metadata?
13
+
14
+ workspace_lib = File.join(PuppetLanguageServerSidecar::Workspace.root_path, 'lib')
15
+ return result unless FileTest.directory?(workspace_lib)
16
+
17
+ result << workspace_lib
18
+
19
+ result
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ # Monkey patch the module loader and inject a workspace module
27
+ # into the modules memoization variable
28
+ require 'puppet/node/environment'
29
+ class Puppet::Node::Environment
30
+ alias original_modules modules
31
+ alias original_modules_by_path modules_by_path
32
+
33
+ # The Puppet::Util::Json class doesn't exist in all puppet version. Instead
34
+ # just vendor the code here as it's a simple JSON loader only for metadata.json.
35
+ # https://github.com/puppetlabs/puppet/blob/5.5.0/lib/puppet/util/json.rb#L32-L49
36
+ def workspace_load_json(string, options = {})
37
+ if defined? MultiJson
38
+ begin
39
+ MultiJson.load(string, options)
40
+ rescue MultiJson::ParseError => e
41
+ raise Puppet::Util::Json::ParseError.build(e, string)
42
+ end
43
+ else
44
+ begin
45
+ string = string.read if string.respond_to?(:read)
46
+
47
+ options[:symbolize_names] = true if options.delete(:symbolize_keys)
48
+ ::JSON.parse(string, options)
49
+ rescue JSON::ParserError => e
50
+ raise Puppet::Util::Json::ParseError.build(e, string)
51
+ end
52
+ end
53
+ end
54
+
55
+ def create_workspace_module_object(path)
56
+ # Read the metadata to find the actual module name
57
+ md_file = File.join(PuppetLanguageServerSidecar::Workspace.root_path, 'metadata.json')
58
+ begin
59
+ metadata = workspace_load_json(File.read(md_file, encoding: 'utf-8'))
60
+ return nil if metadata['name'].nil?
61
+
62
+ # Extract the actual module name
63
+ if Puppet::Module.is_module_directory_name?(metadata['name'])
64
+ module_name = metadata['name']
65
+ elsif Puppet::Module.is_module_namespaced_name?(metadata['name'])
66
+ # Based on regex at https://github.com/puppetlabs/puppet/blob/f5ca8c05174c944f783cfd0b18582e2160b77d0e/lib/puppet/module.rb#L54
67
+ result = /^[a-zA-Z0-9]+-([a-z][a-z0-9_]*)$/.match(metadata['name'])
68
+ module_name = result[1]
69
+ else
70
+ # TODO: This is an invalid puppet module name in the metadata.json. Should we log an error/warning?
71
+ return nil
72
+ end
73
+
74
+ # The Puppet::Module initializer was changed in
75
+ # https://github.com/puppetlabs/puppet/commit/935c0311dbaf1df03937822525c36b26de5390ef
76
+ # We need to switch the creation based on whether the modules_strict_semver? method is available
77
+ return Puppet::Module.new(module_name, path, self, modules_strict_semver?) if respond_to?(:modules_strict_semver?)
78
+
79
+ Puppet::Module.new(module_name, path, self)
80
+ rescue StandardError
81
+ nil
82
+ end
83
+ end
84
+
85
+ def modules
86
+ if @modules.nil?
87
+ original_modules
88
+ if PuppetLanguageServerSidecar::Workspace.has_module_metadata?
89
+ workspace_module = create_workspace_module_object(PuppetLanguageServerSidecar::Workspace.root_path)
90
+ @modules << workspace_module unless workspace_module.nil?
91
+ end
92
+
93
+ @modules
94
+ else
95
+ original_modules
96
+ end
97
+ end
98
+
99
+ def modules_by_path
100
+ result = original_modules_by_path
101
+
102
+ result.keys.each do |key|
103
+ if key == PuppetLanguageServerSidecar::Workspace.root_path && PuppetLanguageServerSidecar::Workspace.has_module_metadata?
104
+ workspace_module = create_workspace_module_object(key)
105
+ result[key] = workspace_module.nil? ? [] : [workspace_module]
106
+ end
107
+ end
108
+
109
+ result
110
+ end
111
+ end
112
+
113
+ # Inject the workspace as a module in all modulepaths
114
+ require 'puppet/settings/environment_conf'
115
+ class Puppet::Settings::EnvironmentConf
116
+ alias original_modulepath modulepath
117
+
118
+ def modulepath
119
+ result = original_modulepath
120
+
121
+ if PuppetLanguageServerSidecar::Workspace.has_module_metadata? # rubocop:disable Style/IfUnlessModifier Nicer to read like this
122
+ result = result + File::PATH_SEPARATOR + PuppetLanguageServerSidecar::Workspace.root_path
123
+ end
124
+
125
+ result
126
+ end
127
+ end
128
+
129
+ # Inject the workspace into the facter search paths
130
+ require 'puppet/indirector/facts/facter'
131
+ class Puppet::Node::Facts::Facter
132
+ class << self
133
+ alias original_setup_search_paths setup_search_paths
134
+ def setup_search_paths(request)
135
+ result = original_setup_search_paths(request)
136
+ return result unless PuppetLanguageServerSidecar::Workspace.has_module_metadata?
137
+
138
+ additional_dirs = %w[lib plugins].map { |path| File.join(PuppetLanguageServerSidecar::Workspace.root_path, path, 'facter') }
139
+ .select { |path| FileTest.directory?(path) }
140
+
141
+ return result if additional_dirs.empty?
142
+
143
+ Facter.search(*additional_dirs)
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # MUST BE LAST!!!!!!
4
+ # Suppress any warning messages to STDOUT. It can pollute stdout when running in STDIO mode
5
+ Puppet::Util::Log.newdesttype :null_logger do
6
+ def handle(msg)
7
+ PuppetLanguageServerSidecar.log_message(:debug, "[PUPPET LOG] [#{msg.level}] #{msg.message}")
8
+ end
9
+ end