puppet-lint-optional_default-check 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '0859f8a9068f1cb24f9add5622e123bdce3bd32bdf686c2445442806add79dad'
4
+ data.tar.gz: 212ef6664c4ad98b77160e490d72b69e2404c8c32cff3de0a4265e380579c1fe
5
+ SHA512:
6
+ metadata.gz: 751ab59ba4cd193955767a5b6bc8090f871ef204a2c7ad52dc8b286936d443c775800b677723d85f4b0619bb164a5da58782dc0ee8c332090f589f6ac6b5c1c4
7
+ data.tar.gz: f24cb90b805fbc43e91de287a22ba7beefe578d5c0c82947526af5e56bf36cd286d94c3987d22d8886c7270bc04d6ff13b53c3c630abc86964c770620b1b9dd9
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Alexander Fisher
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,68 @@
1
+ puppet-lint-optional\_default-check
2
+ ===================================
3
+
4
+ A puppet-lint plugin to check that `Optional` parameters don't default to something other than `undef`.
5
+
6
+ ## Installing
7
+
8
+ ### From the command line
9
+
10
+ ```shell
11
+ $ gem install puppet-lint-optional_default-check
12
+ ```
13
+
14
+ ### In a Gemfile
15
+
16
+ ```ruby
17
+ gem 'puppet-lint-optional_default-check', :require => false
18
+ ```
19
+
20
+ ## Checks
21
+
22
+ ### `Optional` parameter defaults to something other than `undef`
23
+
24
+ An `Optional` parameter in Puppet is one where `undef` is an allowed value.
25
+
26
+ It is normally a mistake to set the default of an `Optional` parameter to something other than `undef`.
27
+ This is because it's not possible to 'pass' `undef` as the value to use for a parameter when declaring a class or defined type.
28
+ When you try to set a parameter to `undef`, Puppet actually uses the class's default value for that parameter, not `undef` itself.
29
+
30
+ (The caveat is that it is possible to use hiera to override a non `undef` default back to `undef`, but in practice, doing this is quite rare.)
31
+
32
+ A **defined type** with an mandatory (no default), `Optional` parameter will raise a warning.
33
+
34
+ The plugin will not raise a warning if a **class** `Optional` parameter doesn't have a default.
35
+ Mandatory parameters can have defaults set in hiera, and several modules *do* use `~` for this.
36
+
37
+ #### What you have done
38
+
39
+ ```puppet
40
+ class foo (
41
+ Optional[Integer] $port = 8080,
42
+ ){
43
+ }
44
+ ```
45
+
46
+ #### What you should have done
47
+
48
+ ```puppet
49
+ class foo (
50
+ Integer $port = 8080,
51
+ ){
52
+ }
53
+ ```
54
+
55
+ or
56
+
57
+ ```puppet
58
+ class foo (
59
+ Optional[Integer] $port = undef,
60
+ ){
61
+ }
62
+ ```
63
+
64
+ # Copyright
65
+
66
+ Copyright 2021 Alexander Fisher
67
+
68
+ Licensed under the MIT License.
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ PuppetLint.new_check(:optional_default) do
4
+ def check
5
+ class_indexes.concat(defined_type_indexes).each do |idx|
6
+ params = extract_params(idx)
7
+ params.each do |param|
8
+ default_value = extract_default_value_tokens(param)
9
+ type = extract_type_tokens(param)
10
+
11
+ if type.size.positive? && # The parameter has a type
12
+ type[0].type == :TYPE && type[0].value == 'Optional' && # That type is Optional
13
+ default_value.size.positive? && # There is a default set
14
+ (default_value.map(&:type) & %i[DOT LPAREN]).none? && # That default doesn't contain a call to a function
15
+ default_value[0].type != :UNDEF && # It isn't undef
16
+ default_value[0].type != :VARIABLE # and it isn't a variable
17
+
18
+ notify(
19
+ :warning,
20
+ message: 'Optional parameter defaults to something other than undef',
21
+ line: param.line,
22
+ column: param.column
23
+ )
24
+ end
25
+
26
+ # If a defined type has an Optional parameter without a default, this is definately an issue as, unlike classes,
27
+ # the default can't actually be coming from hiera.
28
+ next unless idx[:type] == :DEFINE &&
29
+ type.size.positive? &&
30
+ type[0].type == :TYPE && type[0].value == 'Optional' &&
31
+ default_value.size.zero?
32
+
33
+ notify(
34
+ :warning,
35
+ message: 'Optional defined type parameter doesn\'t have a default',
36
+ line: param.line,
37
+ column: param.column
38
+ )
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # Returns an array of parameter tokens
46
+ def extract_params(idx)
47
+ params = []
48
+ return params if idx[:param_tokens].nil?
49
+
50
+ e = idx[:param_tokens].each
51
+ begin
52
+ while (ptok = e.next)
53
+ next unless ptok.type == :VARIABLE
54
+
55
+ params << ptok
56
+ nesting = 0
57
+ # skip to the next parameter to avoid finding default values of variables
58
+ loop do
59
+ ptok = e.next
60
+ case ptok.type
61
+ when :LPAREN, :LBRACK
62
+ nesting += 1
63
+ when :RPAREN, :RBRACK
64
+ nesting -= 1
65
+ when :COMMA
66
+ break unless nesting.positive?
67
+ end
68
+ end
69
+ end
70
+ rescue StopIteration; end # rubocop:disable Lint/SuppressedException
71
+ params
72
+ end
73
+
74
+ # Returns array of tokens that cover the value that the parameter token has as its default
75
+ # Search forward to find value assigned to this parameter
76
+ # We want to find the thing after `=` and before `,`
77
+ def extract_default_value_tokens(ptok)
78
+ value_tokens = []
79
+ token = ptok.next_code_token
80
+ nesting = 0
81
+ while token
82
+ case token.type
83
+ when :LPAREN, :LBRACK
84
+ nesting += 1
85
+ when :RBRACK
86
+ nesting -= 1
87
+ when :RPAREN
88
+ nesting -= 1
89
+ if nesting.negative?
90
+ # This is the RPAREN at the end of the parameters. There wasn't a COMMA
91
+ last_token = token.prev_code_token
92
+ break
93
+ end
94
+ when :EQUALS
95
+ first_token = token.next_code_token
96
+ when :COMMA
97
+ unless nesting.positive?
98
+ last_token = token.prev_code_token
99
+ break
100
+ end
101
+ end
102
+ token = token.next_token
103
+ end
104
+ value_tokens = tokens[tokens.find_index(first_token)..tokens.find_index(last_token)] if first_token && last_token
105
+ value_tokens
106
+ end
107
+
108
+ # Returns an array of tokens that cover the data type of the parameter ptok
109
+ # Search backwards until we either bump into a comma (whilst not nested), or reach the opening LPAREN
110
+ def extract_type_tokens(ptok)
111
+ type_tokens = []
112
+ token = ptok.prev_code_token
113
+ nesting = 0
114
+ while token
115
+ case token.type
116
+ when :LBRACK
117
+ nesting += 1
118
+ when :LPAREN
119
+ nesting += 1
120
+ if nesting.positive?
121
+ # This is the LPAREN at the start of the parameter list
122
+ first_token = token.next_code_token
123
+ last_token = ptok.prev_code_token
124
+ break
125
+ end
126
+ when :RBRACK, :RPAREN
127
+ nesting -= 1
128
+ when :COMMA
129
+ if nesting.zero?
130
+ first_token = token.next_code_token
131
+ last_token = ptok.prev_code_token
132
+ break
133
+ end
134
+ end
135
+
136
+ token = token.prev_code_token
137
+ end
138
+ type_tokens = tokens[tokens.find_index(first_token)..tokens.find_index(last_token)] if first_token && last_token
139
+ type_tokens
140
+ end
141
+ end
@@ -0,0 +1,200 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'optional_default' do
6
+ let(:msg) { 'Optional parameter defaults to something other than undef' }
7
+
8
+ %w[define class].each do |type|
9
+ context "#{type} with Optional parameter defaulting to string on single line" do
10
+ let(:code) { "#{type} test(Optional $foo = 'test') { }" }
11
+
12
+ it 'detects a single problem' do
13
+ expect(problems).to have(1).problem
14
+ end
15
+
16
+ col = (type == 'class' ? 21 : 22)
17
+ it 'creates a warning' do
18
+ expect(problems).to contain_warning(msg).on_line(1).in_column(col)
19
+ end
20
+
21
+ context 'with trailing comma' do
22
+ let(:code) { "#{type} test(Optional $foo = 'test',) { }" }
23
+
24
+ it 'detects a single problem' do
25
+ expect(problems).to have(1).problem
26
+ end
27
+
28
+ col = (type == 'class' ? 21 : 22)
29
+ it 'creates a warning' do
30
+ expect(problems).to contain_warning(msg).on_line(1).in_column(col)
31
+ end
32
+ end
33
+ end
34
+
35
+ context "#{type} with Optional parameter defaulting to string on multiple lines" do
36
+ let(:code) do
37
+ <<~CODE
38
+ #{type} test(
39
+ Optional $foo = 'test'
40
+ ){
41
+ }
42
+ CODE
43
+ end
44
+
45
+ it 'detects a single problem' do
46
+ expect(problems).to have(1).problem
47
+ end
48
+
49
+ it 'creates a warning' do
50
+ expect(problems).to contain_warning(msg).on_line(2).in_column(12)
51
+ end
52
+ end
53
+
54
+ context "#{type} with Optional parameter defaulting to undef on single line" do
55
+ let(:code) { "#{type} test(Optional $foo = undef) { }" }
56
+
57
+ it 'detects no problems' do
58
+ expect(problems).to have(0).problem
59
+ end
60
+
61
+ context 'with trailing comma' do
62
+ let(:code) { "#{type} test(Optional $foo = undef) { }" }
63
+
64
+ it 'detects no problems' do
65
+ expect(problems).to have(0).problem
66
+ end
67
+ end
68
+ end
69
+
70
+ context "#{type} with Optional[String[1]] parameter defaulting to string on single line" do
71
+ let(:code) { "#{type} test(Optional[String[1]] $foo = 'test') { }" }
72
+
73
+ it 'detects a single problem' do
74
+ expect(problems).to have(1).problem
75
+ end
76
+
77
+ col = (type == 'class' ? 32 : 33)
78
+ it 'creates a warning' do
79
+ expect(problems).to contain_warning(msg).on_line(1).in_column(col)
80
+ end
81
+ end
82
+
83
+ context "#{type} with a mandatory parameter followed by an Optional[Hash] parameter with a non undef default" do
84
+ let(:code) do
85
+ <<~CODE
86
+ #{type} test(
87
+ String $foo,
88
+ Optional[Hash] $bar = {
89
+ 'a' => 'b',
90
+ 'c' => 'd'
91
+ },
92
+ ){
93
+ }
94
+ CODE
95
+ end
96
+
97
+ it 'detects a single problem' do
98
+ expect(problems).to have(1).problem
99
+ end
100
+
101
+ it 'creates a warning' do
102
+ expect(problems).to contain_warning(msg).on_line(3).in_column(18)
103
+ end
104
+ end
105
+
106
+ context "#{type} with a mandatory parameter followed by an Optional[Hash] parameter that default to the result of a function" do
107
+ let(:code) do
108
+ <<~CODE
109
+ #{type} test(
110
+ String $foo,
111
+ Optional[Hash] $bar = some::func('foo'),
112
+ ){
113
+ }
114
+ CODE
115
+ end
116
+
117
+ it 'detects no problems' do
118
+ expect(problems).to have(0).problem
119
+ end
120
+ end
121
+
122
+ context "#{type} with a mandatory parameter followed by an Optional[Hash] parameter that default to the result of a function called with dot notation" do
123
+ let(:code) do
124
+ <<~CODE
125
+ #{type} test(
126
+ String $foo,
127
+ Optional[Hash] $bar = 'foo'.func,
128
+ ){
129
+ }
130
+ CODE
131
+ end
132
+
133
+ it 'detects no problems' do
134
+ expect(problems).to have(0).problem
135
+ end
136
+ end
137
+
138
+ context "Complex #{type} with multiple issues and comments" do
139
+ let(:code) do
140
+ <<~CODE
141
+ #{type} test (
142
+ $a, # No type or default
143
+ String $b = 'somestring',
144
+ Optional[String[1]] $c = 'foobar', # This should generate a warning
145
+ Optional[Enum[
146
+ 'a',
147
+ 'b',
148
+ ]
149
+ ] $fuz = 'a', # As should this
150
+ Optional $d = $foo::param::c,
151
+ Optional[Array[String]] $e = [] # Another warning here (also note no trailing comma!)
152
+ ){
153
+ notice('test')
154
+ }
155
+ CODE
156
+ end
157
+
158
+ it 'detects 3 problems' do
159
+ expect(problems).to have(3).problem
160
+ end
161
+
162
+ it { expect(problems).to contain_warning(msg).on_line(4).in_column(27) }
163
+ it { expect(problems).to contain_warning(msg).on_line(9).in_column(27) }
164
+ it { expect(problems).to contain_warning(msg).on_line(11).in_column(27) }
165
+ end
166
+
167
+ context "#{type} with Optional parameter with array operation" do
168
+ let(:code) do
169
+ <<~CODE
170
+ #{type} test(
171
+ Optional[Array] $hostnames = ['foo.example.com', 'bar.example.com'] - $facts['fqdn'],
172
+ ){
173
+ }
174
+ CODE
175
+ end
176
+
177
+ it 'detects a single problem' do
178
+ expect(problems).to have(1).problem
179
+ end
180
+
181
+ it { expect(problems).to contain_warning(msg).on_line(2).in_column(19) }
182
+ end
183
+ end
184
+ context 'with class with Optional parameter with no apparent default' do
185
+ let(:code) { 'class test(Optional $foo) { }' }
186
+
187
+ # There's a good chance the parameter default is actually in hiera as `~`.
188
+ it 'detects no problems' do
189
+ expect(problems).to have(0).problem
190
+ end
191
+ end
192
+
193
+ context 'with defined type with Optional parameter with no default' do
194
+ let(:code) { 'define test(Optional $foo) { }' }
195
+
196
+ it 'detects a single problem' do
197
+ expect(problems).to have(1).problem
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puppet-lint'
4
+
5
+ PuppetLint::Plugins.load_spec_helper
metadata ADDED
@@ -0,0 +1,196 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: puppet-lint-optional_default-check
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Alexander Fisher
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-04-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: puppet-lint
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '2.1'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: rspec
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec-its
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rspec-collection_matchers
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: coveralls
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.7'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.7'
103
+ - !ruby/object:Gem::Dependency
104
+ name: rubocop
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 1.11.0
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: 1.11.0
117
+ - !ruby/object:Gem::Dependency
118
+ name: rubocop-rspec
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: 2.2.0
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: 2.2.0
131
+ - !ruby/object:Gem::Dependency
132
+ name: rubocop-rake
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: 0.5.1
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: 0.5.1
145
+ - !ruby/object:Gem::Dependency
146
+ name: rubocop-performance
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: 1.10.2
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: 1.10.2
159
+ description: " A puppet-lint plugin to check that Optional class/defined type parameters
160
+ don't default to anything other than `undef`.\n"
161
+ email: alex@linfratech.co.uk
162
+ executables: []
163
+ extensions: []
164
+ extra_rdoc_files: []
165
+ files:
166
+ - LICENSE
167
+ - README.md
168
+ - lib/puppet-lint/plugins/check_optional_default.rb
169
+ - spec/puppet-lint/plugins/check_optional_default_spec.rb
170
+ - spec/spec_helper.rb
171
+ homepage: https://github.com/alexjfisher/puppet-lint-optional_default-check
172
+ licenses:
173
+ - MIT
174
+ metadata: {}
175
+ post_install_message:
176
+ rdoc_options: []
177
+ require_paths:
178
+ - lib
179
+ required_ruby_version: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - ">="
182
+ - !ruby/object:Gem::Version
183
+ version: 2.4.0
184
+ required_rubygems_version: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ requirements: []
190
+ rubygems_version: 3.1.6
191
+ signing_key:
192
+ specification_version: 4
193
+ summary: A puppet-lint plugin to check Optional parameters default to `undef`
194
+ test_files:
195
+ - spec/puppet-lint/plugins/check_optional_default_spec.rb
196
+ - spec/spec_helper.rb