Skip to content

Commit 85f123c

Browse files
authored
Merge pull request #25 from glennsarti/add-puppetfile-support
(GH-28) Add basic puppetfile support
2 parents 3fdc7bb + 0445b5f commit 85f123c

File tree

12 files changed

+722
-11
lines changed

12 files changed

+722
-11
lines changed

lib/puppet-languageserver/providers.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
manifest/definition_provider
55
manifest/validation_provider
66
manifest/hover_provider
7+
puppetfile/r10k/module/base
8+
puppetfile/r10k/module/forge
9+
puppetfile/r10k/module/invalid
10+
puppetfile/r10k/module/local
11+
puppetfile/r10k/module/git
12+
puppetfile/r10k/module/svn
13+
puppetfile/r10k/puppetfile
14+
puppetfile/validation_provider
715
].each do |lib|
816
begin
917
require "puppet-languageserver/#{lib}"
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
module PuppetLanguageServer
2+
module Puppetfile
3+
module R10K
4+
module Module
5+
def self.from_puppetfile(title, args)
6+
return Git.new(title, args) if Git.implements?(title, args)
7+
return Svn.new(title, args) if Svn.implements?(title, args)
8+
return Local.new(title, args) if Local.implements?(title, args)
9+
return Forge.new(title, args) if Forge.implements?(title, args)
10+
11+
Invalid.new(title, args)
12+
end
13+
14+
class Base
15+
# The full title of the module
16+
attr_reader :title
17+
18+
# The name of the module
19+
attr_reader :name
20+
21+
# The line number where this module is first found in Puppetfile
22+
attr_reader :puppetfile_line_number
23+
24+
def initialize(title, args)
25+
@title = title
26+
@args = args
27+
@owner, @name = parse_title(@title)
28+
29+
@puppetfile_line_number = find_load_location
30+
end
31+
32+
# Should be overridden in concrete module classes
33+
def version
34+
nil
35+
end
36+
37+
# Should be overridden in concrete module classes
38+
def properties
39+
{}
40+
end
41+
42+
private
43+
44+
def parse_title(title)
45+
if (match = title.match(/\A(\w+)\Z/))
46+
[nil, match[1]]
47+
elsif (match = title.match(/\A(\w+)[-\/](\w+)\Z/))
48+
[match[1], match[2]]
49+
else
50+
raise ArgumentError, format("Module name (%<title>s) must match either 'modulename' or 'owner/modulename'", title: title)
51+
end
52+
end
53+
54+
def find_load_location
55+
loc = Kernel.caller_locations
56+
.find { |call_loc| call_loc.absolute_path == PuppetLanguageServer::Puppetfile::R10K::PUPPETFILE_MONIKER }
57+
loc.nil? ? 0 : loc.lineno - 1 # Line numbers from ruby are base 1
58+
end
59+
end
60+
end
61+
end
62+
end
63+
end
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
module PuppetLanguageServer
2+
module Puppetfile
3+
module R10K
4+
module Module
5+
class Forge < PuppetLanguageServer::Puppetfile::R10K::Module::Base
6+
def self.implements?(name, args)
7+
!name.match(/\A(\w+)[-\/](\w+)\Z/).nil? && valid_version?(args)
8+
end
9+
10+
def self.valid_version?(value)
11+
return false unless value.is_a?(String) || value.is_a?(Symbol)
12+
value == :latest || value.nil? || valid_version_string?(value)
13+
end
14+
15+
def properties
16+
{
17+
:type => :forge
18+
}
19+
end
20+
21+
# Version string matching regexes
22+
# From Semantic Puppet gem
23+
REGEX_NUMERIC = '(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)'.freeze # Major . Minor . Patch
24+
REGEX_PRE = '(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?'.freeze # Prerelease
25+
REGEX_BUILD = '(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?'.freeze # Build
26+
REGEX_FULL = REGEX_NUMERIC + REGEX_PRE + REGEX_BUILD.freeze
27+
REGEX_FULL_RX = /\A#{REGEX_FULL}\Z/
28+
29+
def self.valid_version_string?(value)
30+
match = value.match(REGEX_FULL_RX)
31+
if match.nil?
32+
false
33+
else
34+
prerelease = match[4]
35+
prerelease.nil? || prerelease.split('.').all? { |x| x !~ /^0\d+$/ }
36+
end
37+
end
38+
end
39+
end
40+
end
41+
end
42+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module PuppetLanguageServer
2+
module Puppetfile
3+
module R10K
4+
module Module
5+
class Git < PuppetLanguageServer::Puppetfile::R10K::Module::Base
6+
def self.implements?(_name, args)
7+
args.is_a?(Hash) && args.key?(:git)
8+
rescue StandardError
9+
false
10+
end
11+
12+
def properties
13+
{
14+
:type => :git
15+
}
16+
end
17+
end
18+
end
19+
end
20+
end
21+
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# This is a special module definition. It's the catchall when no other module type can handle it
2+
3+
module PuppetLanguageServer
4+
module Puppetfile
5+
module R10K
6+
module Module
7+
class Invalid < PuppetLanguageServer::Puppetfile::R10K::Module::Base
8+
def self.implements?(_name, _args)
9+
true
10+
end
11+
12+
def initialize(title, args)
13+
super
14+
@error_message = format("Module %<title>s with args %<args>s doesn't have an implementation. (Are you using the right arguments?)", title: title, args: args.inspect)
15+
end
16+
17+
def properties
18+
{
19+
:type => :invalid,
20+
:error_message => @error_message
21+
}
22+
end
23+
end
24+
end
25+
end
26+
end
27+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module PuppetLanguageServer
2+
module Puppetfile
3+
module R10K
4+
module Module
5+
class Local < PuppetLanguageServer::Puppetfile::R10K::Module::Base
6+
def self.implements?(_name, args)
7+
args.is_a?(Hash) && args.key?(:local)
8+
rescue StandardError
9+
false
10+
end
11+
12+
def properties
13+
{
14+
:type => :local
15+
}
16+
end
17+
end
18+
end
19+
end
20+
end
21+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module PuppetLanguageServer
2+
module Puppetfile
3+
module R10K
4+
module Module
5+
class Svn < PuppetLanguageServer::Puppetfile::R10K::Module::Base
6+
def self.implements?(_name, args)
7+
args.is_a?(Hash) && args.key?(:svn)
8+
rescue StandardError
9+
false
10+
end
11+
12+
def properties
13+
{
14+
:type => :svn
15+
}
16+
end
17+
end
18+
end
19+
end
20+
end
21+
end
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
module PuppetLanguageServer
2+
module Puppetfile
3+
module R10K
4+
PUPPETFILE_MONIKER ||= 'Puppetfile'.freeze
5+
6+
class Puppetfile
7+
attr_reader :modules
8+
9+
def load!(puppetfile_contents)
10+
puppetfile = DSL.new(self)
11+
@modules = []
12+
puppetfile.instance_eval(puppetfile_contents, PUPPETFILE_MONIKER)
13+
end
14+
15+
def add_module(name, args)
16+
@modules << Module.from_puppetfile(name, args)
17+
end
18+
19+
class DSL
20+
def initialize(parent)
21+
@parent = parent
22+
end
23+
24+
# @param [String] name
25+
# @param [*Object] args
26+
def mod(name, args = nil)
27+
@parent.add_module(name, args)
28+
end
29+
30+
# @param [String] forge
31+
def forge(_location)
32+
end
33+
34+
# @param [String] moduledir
35+
def moduledir(_location)
36+
end
37+
38+
def method_missing(method, *_args) # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing
39+
raise NoMethodError, format("Unknown method '%<method>s'", method: method)
40+
end
41+
end
42+
end
43+
end
44+
end
45+
end
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
# Check for invalid module definitions
40+
puppetfile.modules.each do |mod|
41+
next unless mod.properties[:type] == :invalid
42+
# Note - Ruby doesn't give a character position so just highlight the entire line
43+
result << LanguageServer::Diagnostic.create('severity' => LanguageServer::DIAGNOSTICSEVERITY_ERROR,
44+
'fromline' => mod.puppetfile_line_number,
45+
'toline' => mod.puppetfile_line_number,
46+
'fromchar' => 0,
47+
'tochar' => max_line_length,
48+
'source' => 'Puppet',
49+
'message' => mod.properties[:error_message])
50+
end
51+
52+
# Check for duplicate module definitions
53+
dupes = puppetfile.modules
54+
.group_by { |mod| mod.name }
55+
.select { |_, v| v.size > 1 }
56+
.map(&:first)
57+
dupes.each do |dupe_module_name|
58+
puppetfile.modules.select { |mod| mod.name == dupe_module_name }.each do |puppet_module|
59+
# Note - Ruby doesn't give a character position so just highlight the entire line
60+
result << LanguageServer::Diagnostic.create('severity' => LanguageServer::DIAGNOSTICSEVERITY_ERROR,
61+
'fromline' => puppet_module.puppetfile_line_number,
62+
'toline' => puppet_module.puppetfile_line_number,
63+
'fromchar' => 0,
64+
'tochar' => max_line_length,
65+
'source' => 'Puppet',
66+
'message' => "Duplicate module definition for '#{puppet_module.name}'")
67+
end
68+
end
69+
70+
result
71+
end
72+
end
73+
end
74+
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

0 commit comments

Comments
 (0)