puppet-lint-optional_default-check 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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