openvox-lint 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 +7 -0
- data/CHANGELOG.md +52 -0
- data/DOCUMENTATION.md +481 -0
- data/LICENSE +83 -0
- data/README.md +497 -0
- data/bin/openvox-lint +7 -0
- data/lib/openvox-lint/check_plugin.rb +147 -0
- data/lib/openvox-lint/checks.rb +46 -0
- data/lib/openvox-lint/cli.rb +87 -0
- data/lib/openvox-lint/configuration.rb +59 -0
- data/lib/openvox-lint/lexer.rb +342 -0
- data/lib/openvox-lint/linter.rb +72 -0
- data/lib/openvox-lint/plugins/checks/arrow_alignment.rb +42 -0
- data/lib/openvox-lint/plugins/checks/autoloader_layout.rb +31 -0
- data/lib/openvox-lint/plugins/checks/case_without_default.rb +28 -0
- data/lib/openvox-lint/plugins/checks/class_inherits_params.rb +13 -0
- data/lib/openvox-lint/plugins/checks/documentation.rb +26 -0
- data/lib/openvox-lint/plugins/checks/double_quoted_strings.rb +19 -0
- data/lib/openvox-lint/plugins/checks/duplicate_params.rb +24 -0
- data/lib/openvox-lint/plugins/checks/ensure_first_param.rb +28 -0
- data/lib/openvox-lint/plugins/checks/ensure_not_symlink_target.rb +29 -0
- data/lib/openvox-lint/plugins/checks/file_mode.rb +33 -0
- data/lib/openvox-lint/plugins/checks/hard_tabs.rb +15 -0
- data/lib/openvox-lint/plugins/checks/hiera3_function.rb +16 -0
- data/lib/openvox-lint/plugins/checks/import_statement.rb +13 -0
- data/lib/openvox-lint/plugins/checks/inherits_across_namespaces.rb +27 -0
- data/lib/openvox-lint/plugins/checks/leading_zero.rb +22 -0
- data/lib/openvox-lint/plugins/checks/legacy_facts.rb +47 -0
- data/lib/openvox-lint/plugins/checks/line_length.rb +18 -0
- data/lib/openvox-lint/plugins/checks/nested_classes_or_defines.rb +26 -0
- data/lib/openvox-lint/plugins/checks/node_name_unquoted.rb +18 -0
- data/lib/openvox-lint/plugins/checks/only_variable_string.rb +18 -0
- data/lib/openvox-lint/plugins/checks/parameter_order.rb +25 -0
- data/lib/openvox-lint/plugins/checks/puppet_url_without_modules.rb +17 -0
- data/lib/openvox-lint/plugins/checks/quoted_booleans.rb +16 -0
- data/lib/openvox-lint/plugins/checks/relative_classname_inclusion.rb +24 -0
- data/lib/openvox-lint/plugins/checks/resource_reference_without_title_capital.rb +21 -0
- data/lib/openvox-lint/plugins/checks/selector_inside_resource.rb +15 -0
- data/lib/openvox-lint/plugins/checks/single_quote_string_with_variables.rb +16 -0
- data/lib/openvox-lint/plugins/checks/space_before_arrow.rb +20 -0
- data/lib/openvox-lint/plugins/checks/star_comments.rb +13 -0
- data/lib/openvox-lint/plugins/checks/strict_indent.rb +16 -0
- data/lib/openvox-lint/plugins/checks/top_scope_facts.rb +19 -0
- data/lib/openvox-lint/plugins/checks/trailing_comma.rb +24 -0
- data/lib/openvox-lint/plugins/checks/trailing_whitespace.rb +14 -0
- data/lib/openvox-lint/plugins/checks/unquoted_file_mode.rb +24 -0
- data/lib/openvox-lint/plugins/checks/unquoted_resource_title.rb +13 -0
- data/lib/openvox-lint/plugins/checks/variable_contains_dash.rb +15 -0
- data/lib/openvox-lint/plugins/checks/variable_is_lowercase.rb +16 -0
- data/lib/openvox-lint/plugins/checks/variables_not_enclosed.rb +19 -0
- data/lib/openvox-lint/report.rb +86 -0
- data/lib/openvox-lint/token.rb +38 -0
- data/lib/openvox-lint/version.rb +5 -0
- data/lib/openvox-lint.rb +47 -0
- metadata +145 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Classes and defined types should be preceded by documentation comments.
|
|
4
|
+
OpenvoxLint.new_check(:documentation) do
|
|
5
|
+
def check
|
|
6
|
+
tokens.each_with_index do |tok, i|
|
|
7
|
+
next unless tok.type == :CLASS || tok.type == :DEFINE
|
|
8
|
+
# Look backwards for a comment on the preceding line(s)
|
|
9
|
+
has_doc = false
|
|
10
|
+
j = i - 1
|
|
11
|
+
while j >= 0
|
|
12
|
+
prev = tokens[j]
|
|
13
|
+
if prev.type == :COMMENT && prev.line >= tok.line - 2
|
|
14
|
+
has_doc = true; break
|
|
15
|
+
end
|
|
16
|
+
break unless prev.formatting?
|
|
17
|
+
j -= 1
|
|
18
|
+
end
|
|
19
|
+
next if has_doc
|
|
20
|
+
kind = tok.type == :CLASS ? 'class' : 'defined type'
|
|
21
|
+
notify :warning,
|
|
22
|
+
message: "#{kind} not documented (add a comment above the declaration)",
|
|
23
|
+
line: tok.line, column: tok.column
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Double-quoted strings that do not contain variables or escape sequences
|
|
4
|
+
# should use single quotes instead.
|
|
5
|
+
OpenvoxLint.new_check(:double_quoted_strings) do
|
|
6
|
+
def check
|
|
7
|
+
tokens.each do |tok|
|
|
8
|
+
next unless tok.type == :STRING
|
|
9
|
+
val = tok.value
|
|
10
|
+
# STRING type means double-quoted without interpolation
|
|
11
|
+
next if val =~ /\\[nt\\$"]/ # has meaningful escapes
|
|
12
|
+
next if val.length <= 2 # empty string ""
|
|
13
|
+
notify :warning,
|
|
14
|
+
message: 'string does not contain variables or escapes; use single quotes',
|
|
15
|
+
line: tok.line,
|
|
16
|
+
column: tok.column
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# No duplicate parameters in resource declarations.
|
|
4
|
+
OpenvoxLint.new_check(:duplicate_params) do
|
|
5
|
+
def check
|
|
6
|
+
resource_indexes.each do |res|
|
|
7
|
+
seen = {}
|
|
8
|
+
params = res[:param_tokens]
|
|
9
|
+
params.each_with_index do |tok, i|
|
|
10
|
+
next unless tok.type == :NAME
|
|
11
|
+
# Check if followed by =>
|
|
12
|
+
j = i + 1
|
|
13
|
+
j += 1 while j < params.length && params[j].formatting?
|
|
14
|
+
next unless j < params.length && params[j].type == :FARROW
|
|
15
|
+
if seen[tok.value]
|
|
16
|
+
notify :error,
|
|
17
|
+
message: "duplicate parameter '#{tok.value}'",
|
|
18
|
+
line: tok.line, column: tok.column
|
|
19
|
+
end
|
|
20
|
+
seen[tok.value] = true
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The 'ensure' attribute should be the first parameter in a resource body.
|
|
4
|
+
OpenvoxLint.new_check(:ensure_first_param) do
|
|
5
|
+
def check
|
|
6
|
+
resource_indexes.each do |resource|
|
|
7
|
+
params = resource[:param_tokens]
|
|
8
|
+
ensure_idx = nil
|
|
9
|
+
first_param_idx = nil
|
|
10
|
+
params.each_with_index do |tok, i|
|
|
11
|
+
if tok.type == :NAME || tok.type == :CLASSREF
|
|
12
|
+
first_param_idx ||= i
|
|
13
|
+
if tok.value == 'ensure'
|
|
14
|
+
ensure_idx = i
|
|
15
|
+
break
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
next unless ensure_idx
|
|
20
|
+
next if ensure_idx == first_param_idx
|
|
21
|
+
next unless first_param_idx
|
|
22
|
+
notify :warning,
|
|
23
|
+
message: "ensure is not the first attribute (found at position after first param)",
|
|
24
|
+
line: params[ensure_idx].line,
|
|
25
|
+
column: params[ensure_idx].column
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Symlinks should use ensure => link with a target attribute,
|
|
4
|
+
# not ensure => '/path/to/target'.
|
|
5
|
+
OpenvoxLint.new_check(:ensure_not_symlink_target) do
|
|
6
|
+
def check
|
|
7
|
+
tokens.each_with_index do |tok, i|
|
|
8
|
+
next unless tok.type == :NAME && tok.value == 'ensure'
|
|
9
|
+
arrow = find_next_non_ws(i + 1)
|
|
10
|
+
next unless arrow && arrow.type == :FARROW
|
|
11
|
+
val = find_next_non_ws(tokens.index(arrow) + 1)
|
|
12
|
+
next unless val
|
|
13
|
+
next unless (val.type == :SSTRING || val.type == :STRING) && val.value =~ /\//
|
|
14
|
+
notify :warning,
|
|
15
|
+
message: 'ensure should not be set to a symlink target; use ensure => link with a target attribute',
|
|
16
|
+
line: val.line,
|
|
17
|
+
column: val.column
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def find_next_non_ws(start)
|
|
24
|
+
(start...tokens.length).each do |j|
|
|
25
|
+
return tokens[j] unless tokens[j].formatting?
|
|
26
|
+
end
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# File mode should be a 4-digit quoted string or symbolic mode.
|
|
4
|
+
OpenvoxLint.new_check(:file_mode) do
|
|
5
|
+
def check
|
|
6
|
+
tokens.each_with_index do |tok, i|
|
|
7
|
+
next unless tok.type == :NAME && tok.value == 'mode'
|
|
8
|
+
arrow = next_non_ws(i + 1)
|
|
9
|
+
next unless arrow && arrow.type == :FARROW
|
|
10
|
+
val = next_non_ws(tokens.index(arrow) + 1)
|
|
11
|
+
next unless val
|
|
12
|
+
if val.type == :NUMBER
|
|
13
|
+
notify :warning,
|
|
14
|
+
message: "file mode should be a quoted string, not a bare number",
|
|
15
|
+
line: val.line, column: val.column
|
|
16
|
+
elsif val.type == :SSTRING || val.type == :STRING
|
|
17
|
+
mode = val.value.gsub(/['"]/, '')
|
|
18
|
+
next if mode =~ /\A[0-7]{4}\z/ # 4-digit octal
|
|
19
|
+
next if mode =~ /\A[ugoa]+[=+-]/ # symbolic
|
|
20
|
+
notify :warning,
|
|
21
|
+
message: "file mode '#{mode}' is not a valid 4-digit octal or symbolic mode",
|
|
22
|
+
line: val.line, column: val.column
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def next_non_ws(start)
|
|
30
|
+
(start...tokens.length).each { |j| return tokens[j] unless tokens[j].formatting? }
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Detects hard tab characters. Two-space soft tabs are required.
|
|
4
|
+
OpenvoxLint.new_check(:hard_tabs) do
|
|
5
|
+
def check
|
|
6
|
+
manifest_lines.each_with_index do |line, idx|
|
|
7
|
+
col = line.index("\t")
|
|
8
|
+
next unless col
|
|
9
|
+
notify :warning,
|
|
10
|
+
message: 'tab character found (use 2-space soft tabs)',
|
|
11
|
+
line: idx + 1,
|
|
12
|
+
column: col + 1
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Hiera 3 functions (hiera, hiera_array, hiera_hash, hiera_include)
|
|
4
|
+
# are removed in Puppet 8 / OpenVox 8. Use lookup() instead.
|
|
5
|
+
OpenvoxLint.new_check(:hiera3_function) do
|
|
6
|
+
HIERA3_FUNCS = %w[hiera hiera_array hiera_hash hiera_include].freeze
|
|
7
|
+
|
|
8
|
+
def check
|
|
9
|
+
tokens.each do |tok|
|
|
10
|
+
next unless tok.type == :NAME && HIERA3_FUNCS.include?(tok.value)
|
|
11
|
+
notify :error,
|
|
12
|
+
message: "'#{tok.value}()' is removed in Puppet 8 / OpenVox 8 — use lookup() instead",
|
|
13
|
+
line: tok.line, column: tok.column
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The 'import' statement was removed in Puppet 4+.
|
|
4
|
+
OpenvoxLint.new_check(:import_statement) do
|
|
5
|
+
def check
|
|
6
|
+
tokens.each do |tok|
|
|
7
|
+
next unless tok.type == :IMPORT
|
|
8
|
+
notify :error,
|
|
9
|
+
message: "'import' statement was removed in Puppet 4; use module autoloading instead",
|
|
10
|
+
line: tok.line, column: tok.column
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Classes should not inherit across module namespaces.
|
|
4
|
+
OpenvoxLint.new_check(:inherits_across_namespaces) do
|
|
5
|
+
def check
|
|
6
|
+
sem = semantic_tokens
|
|
7
|
+
sem.each_with_index do |tok, i|
|
|
8
|
+
next unless tok.type == :INHERITS
|
|
9
|
+
# The class being inherited is the next name token
|
|
10
|
+
j = i + 1
|
|
11
|
+
next if j >= sem.length
|
|
12
|
+
parent = sem[j]
|
|
13
|
+
next unless parent.type == :NAME || parent.type == :CLASSREF
|
|
14
|
+
# Find the class name (before inherits)
|
|
15
|
+
k = i - 1
|
|
16
|
+
next if k < 0
|
|
17
|
+
child = sem[k]
|
|
18
|
+
next unless child.type == :NAME || child.type == :CLASSREF
|
|
19
|
+
child_ns = child.value.split('::').first
|
|
20
|
+
parent_ns = parent.value.sub(/\A::/, '').split('::').first
|
|
21
|
+
next if child_ns == parent_ns
|
|
22
|
+
notify :warning,
|
|
23
|
+
message: "class '#{child.value}' inherits from '#{parent.value}' across namespaces",
|
|
24
|
+
line: tok.line, column: tok.column
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Numbers should not have leading zeros (except octal file modes).
|
|
4
|
+
OpenvoxLint.new_check(:leading_zero) do
|
|
5
|
+
def check
|
|
6
|
+
tokens.each_with_index do |tok, i|
|
|
7
|
+
next unless tok.type == :NUMBER
|
|
8
|
+
next unless tok.value =~ /\A0\d+\z/ && tok.value !~ /\A0[xX]/
|
|
9
|
+
# Allow if preceded by 'mode =>'
|
|
10
|
+
j = i - 1
|
|
11
|
+
j -= 1 while j >= 0 && tokens[j].formatting?
|
|
12
|
+
if j >= 0 && tokens[j].type == :FARROW
|
|
13
|
+
k = j - 1
|
|
14
|
+
k -= 1 while k >= 0 && tokens[k].formatting?
|
|
15
|
+
next if k >= 0 && tokens[k].type == :NAME && tokens[k].value == 'mode'
|
|
16
|
+
end
|
|
17
|
+
notify :warning,
|
|
18
|
+
message: "number '#{tok.value}' has a leading zero (interpreted as octal)",
|
|
19
|
+
line: tok.line, column: tok.column
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Legacy (unstructured) top-scope facts are removed in Puppet 8 / OpenVox 8.
|
|
4
|
+
# Use structured facts via $facts hash instead.
|
|
5
|
+
#
|
|
6
|
+
# e.g. $osfamily -> $facts['os']['family']
|
|
7
|
+
# $fqdn -> $facts['networking']['fqdn']
|
|
8
|
+
# $ipaddress -> $facts['networking']['ip']
|
|
9
|
+
# $operatingsystem -> $facts['os']['name']
|
|
10
|
+
OpenvoxLint.new_check(:legacy_facts) do
|
|
11
|
+
LEGACY_FACTS = %w[
|
|
12
|
+
architecture augeasversion bios_release_date bios_vendor bios_version
|
|
13
|
+
blockdevice_sda_model blockdevice_sda_size blockdevice_sda_vendor
|
|
14
|
+
blockdevices boardmanufacturer boardproductname boardserialnumber
|
|
15
|
+
chassisassettag chassistype domain facterversion filesystems fqdn
|
|
16
|
+
gid hardwareisa hardwaremodel hostname id interfaces ipaddress
|
|
17
|
+
ipaddress6 is_virtual kernel kernelmajversion kernelrelease
|
|
18
|
+
kernelversion lsbdistcodename lsbdistdescription lsbdistid
|
|
19
|
+
lsbdistrelease lsbmajdistrelease lsbminordistrelease lsbrelease
|
|
20
|
+
macaddress manufacturer memoryfree memorysize memoryfree_mb
|
|
21
|
+
memorysize_mb netmask network operatingsystem operatingsystemmajrelease
|
|
22
|
+
operatingsystemrelease os osfamily path physicalprocessorcount
|
|
23
|
+
processor0 processorcount productname ps puppetversion rubyplatform
|
|
24
|
+
rubysitedir rubyversion selinux selinux_config_mode
|
|
25
|
+
selinux_config_policy selinux_current_mode selinux_enforced
|
|
26
|
+
selinux_policyversion serialnumber sp_boot_mode sp_boot_volume
|
|
27
|
+
sp_cpu_type sp_current_processor_speed sp_l2_cache_core
|
|
28
|
+
sp_l3_cache sp_local_host_name sp_machine_model sp_machine_name
|
|
29
|
+
sp_number_processors sp_os_version sp_packages sp_physical_memory
|
|
30
|
+
sp_platform_uuid sp_secure_vm sp_serial_number sp_smc_version_system
|
|
31
|
+
sp_uptime sshdsakey sshecdsakey sshed25519key sshfp_dsa sshfp_ecdsa
|
|
32
|
+
sshfp_ed25519 sshfp_rsa sshrsakey swapfree swapfree_mb swapsize
|
|
33
|
+
swapsize_mb system_uptime timezone type uniqueid uptime
|
|
34
|
+
uptime_days uptime_hours uptime_seconds uuid virtual
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
def check
|
|
38
|
+
tokens.each do |tok|
|
|
39
|
+
next unless tok.type == :VARIABLE
|
|
40
|
+
name = tok.value.sub(/^\$/, '').sub(/\A::/, '')
|
|
41
|
+
next unless LEGACY_FACTS.include?(name)
|
|
42
|
+
notify :warning,
|
|
43
|
+
message: "legacy fact '#{name}' — use $facts['...'] structured fact instead (Puppet 8 / OpenVox 8)",
|
|
44
|
+
line: tok.line, column: tok.column
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Lines should not exceed 140 characters.
|
|
4
|
+
OpenvoxLint.new_check(:line_length) do
|
|
5
|
+
MAX_LENGTH = 140
|
|
6
|
+
|
|
7
|
+
def check
|
|
8
|
+
manifest_lines.each_with_index do |line, idx|
|
|
9
|
+
next if line.length <= MAX_LENGTH
|
|
10
|
+
# Exception: long puppet:/// source URLs
|
|
11
|
+
next if line =~ /puppet:\/\//
|
|
12
|
+
notify :warning,
|
|
13
|
+
message: "line has #{line.length} characters (max #{MAX_LENGTH})",
|
|
14
|
+
line: idx + 1,
|
|
15
|
+
column: MAX_LENGTH + 1
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Classes and defined types should not be nested inside other classes or defined types.
|
|
4
|
+
OpenvoxLint.new_check(:nested_classes_or_defines) do
|
|
5
|
+
def check
|
|
6
|
+
depth = 0
|
|
7
|
+
in_class_or_define = false
|
|
8
|
+
tokens.each do |tok|
|
|
9
|
+
if tok.type == :CLASS || tok.type == :DEFINE
|
|
10
|
+
if in_class_or_define && depth > 0
|
|
11
|
+
kind = tok.type == :CLASS ? 'class' : 'defined type'
|
|
12
|
+
notify :warning,
|
|
13
|
+
message: "#{kind} defined inside another class or defined type",
|
|
14
|
+
line: tok.line, column: tok.column
|
|
15
|
+
end
|
|
16
|
+
in_class_or_define = true
|
|
17
|
+
end
|
|
18
|
+
case tok.type
|
|
19
|
+
when :LBRACE then depth += 1
|
|
20
|
+
when :RBRACE
|
|
21
|
+
depth -= 1
|
|
22
|
+
in_class_or_define = false if depth == 0
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Node names should be quoted strings, not bare words.
|
|
4
|
+
OpenvoxLint.new_check(:node_name_unquoted) do
|
|
5
|
+
def check
|
|
6
|
+
sem = semantic_tokens
|
|
7
|
+
sem.each_with_index do |tok, i|
|
|
8
|
+
next unless tok.type == :NODE
|
|
9
|
+
j = i + 1
|
|
10
|
+
next if j >= sem.length
|
|
11
|
+
name = sem[j]
|
|
12
|
+
next unless name.type == :NAME
|
|
13
|
+
notify :warning,
|
|
14
|
+
message: "unquoted node name '#{name.value}' — use a quoted string",
|
|
15
|
+
line: name.line, column: name.column
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A string containing only a variable should not be quoted.
|
|
4
|
+
# e.g. "${foo}" should be $foo
|
|
5
|
+
OpenvoxLint.new_check(:only_variable_string) do
|
|
6
|
+
def check
|
|
7
|
+
tokens.each do |tok|
|
|
8
|
+
next unless tok.type == :DQSTRING
|
|
9
|
+
val = tok.value
|
|
10
|
+
# Match strings like "${varname}" or "$varname" with nothing else
|
|
11
|
+
next unless val =~ /\A"\$\{?[a-zA-Z_][a-zA-Z0-9_:]*\}?"\z/
|
|
12
|
+
notify :warning,
|
|
13
|
+
message: 'string containing only a variable is unnecessarily quoted',
|
|
14
|
+
line: tok.line,
|
|
15
|
+
column: tok.column
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Parameters with defaults should come after parameters without defaults.
|
|
4
|
+
OpenvoxLint.new_check(:parameter_order) do
|
|
5
|
+
def check
|
|
6
|
+
[class_indexes, defined_type_indexes].flatten.each do |defn|
|
|
7
|
+
params = defn[:param_tokens]
|
|
8
|
+
found_default = false
|
|
9
|
+
params.each_with_index do |tok, i|
|
|
10
|
+
next unless tok.type == :VARIABLE
|
|
11
|
+
# Look ahead for = (default value)
|
|
12
|
+
j = i + 1
|
|
13
|
+
j += 1 while j < params.length && params[j].formatting?
|
|
14
|
+
has_default = j < params.length && params[j].type == :EQUALS
|
|
15
|
+
if has_default
|
|
16
|
+
found_default = true
|
|
17
|
+
elsif found_default
|
|
18
|
+
notify :warning,
|
|
19
|
+
message: "parameter '#{tok.value}' without default follows a parameter with a default",
|
|
20
|
+
line: tok.line, column: tok.column
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# puppet:// URLs should include the modules mount point.
|
|
4
|
+
# e.g. puppet:///modules/mymod/file not puppet:///mymod/file
|
|
5
|
+
OpenvoxLint.new_check(:puppet_url_without_modules) do
|
|
6
|
+
def check
|
|
7
|
+
tokens.each do |tok|
|
|
8
|
+
next unless tok.type == :SSTRING || tok.type == :STRING || tok.type == :DQSTRING
|
|
9
|
+
val = tok.value.gsub(/['"]/, '')
|
|
10
|
+
next unless val.start_with?('puppet:///')
|
|
11
|
+
next if val.start_with?('puppet:///modules/')
|
|
12
|
+
notify :warning,
|
|
13
|
+
message: "puppet:// URL should include /modules/ mount point",
|
|
14
|
+
line: tok.line, column: tok.column
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Booleans should not be quoted strings.
|
|
4
|
+
# e.g. 'true' or "false" should be bare true/false.
|
|
5
|
+
OpenvoxLint.new_check(:quoted_booleans) do
|
|
6
|
+
def check
|
|
7
|
+
tokens.each do |tok|
|
|
8
|
+
next unless tok.type == :SSTRING || tok.type == :STRING
|
|
9
|
+
val = tok.value.gsub(/['"]/, '')
|
|
10
|
+
next unless val == 'true' || val == 'false'
|
|
11
|
+
notify :warning,
|
|
12
|
+
message: "quoted boolean '#{val}' — use bare #{val} instead",
|
|
13
|
+
line: tok.line, column: tok.column
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Use absolute class names in include/require/contain statements.
|
|
4
|
+
OpenvoxLint.new_check(:relative_classname_inclusion) do
|
|
5
|
+
INCLUDE_FUNCS = %w[include require contain].freeze
|
|
6
|
+
|
|
7
|
+
def check
|
|
8
|
+
sem = semantic_tokens
|
|
9
|
+
sem.each_with_index do |tok, i|
|
|
10
|
+
next unless tok.type == :NAME && INCLUDE_FUNCS.include?(tok.value)
|
|
11
|
+
j = i + 1
|
|
12
|
+
next if j >= sem.length
|
|
13
|
+
name_tok = sem[j]
|
|
14
|
+
next unless name_tok.type == :NAME || name_tok.type == :SSTRING || name_tok.type == :STRING
|
|
15
|
+
class_name = name_tok.value.gsub(/['"]/, '')
|
|
16
|
+
next if class_name.start_with?('::') || class_name.empty?
|
|
17
|
+
# Single-segment names are OK (they're unambiguous)
|
|
18
|
+
next unless class_name.include?('::')
|
|
19
|
+
notify :warning,
|
|
20
|
+
message: "class name '#{class_name}' should be fully qualified (start with ::)",
|
|
21
|
+
line: name_tok.line, column: name_tok.column
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Resource reference titles must start with a capital letter.
|
|
4
|
+
# e.g. File['/tmp/foo'] not file['/tmp/foo']
|
|
5
|
+
OpenvoxLint.new_check(:resource_reference_without_title_capital) do
|
|
6
|
+
def check
|
|
7
|
+
sem = semantic_tokens
|
|
8
|
+
sem.each_with_index do |tok, i|
|
|
9
|
+
next unless tok.type == :NAME
|
|
10
|
+
next if i + 1 >= sem.length
|
|
11
|
+
next unless sem[i + 1].type == :LBRACK
|
|
12
|
+
# This is name[...] which should be Name[...] for a resource reference
|
|
13
|
+
next if tok.value =~ /\A[A-Z]/
|
|
14
|
+
# Skip function calls and normal array access
|
|
15
|
+
next if %w[split join include contain require].include?(tok.value)
|
|
16
|
+
notify :warning,
|
|
17
|
+
message: "resource reference '#{tok.value}' must start with a capital letter",
|
|
18
|
+
line: tok.line, column: tok.column
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Selectors (?) should not be used inside resource declarations.
|
|
4
|
+
OpenvoxLint.new_check(:selector_inside_resource) do
|
|
5
|
+
def check
|
|
6
|
+
resource_indexes.each do |res|
|
|
7
|
+
res[:param_tokens].each do |tok|
|
|
8
|
+
next unless tok.type == :QMARK
|
|
9
|
+
notify :warning,
|
|
10
|
+
message: 'selector (?) inside resource body; use a variable or conditional instead',
|
|
11
|
+
line: tok.line, column: tok.column
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Single-quoted strings containing variable-like patterns ($var)
|
|
4
|
+
# should use double quotes for interpolation.
|
|
5
|
+
OpenvoxLint.new_check(:single_quote_string_with_variables) do
|
|
6
|
+
def check
|
|
7
|
+
tokens.each do |tok|
|
|
8
|
+
next unless tok.type == :SSTRING
|
|
9
|
+
val = tok.value
|
|
10
|
+
next unless val =~ /\$[a-zA-Z_]/
|
|
11
|
+
notify :warning,
|
|
12
|
+
message: 'single-quoted string contains a variable reference; use double quotes for interpolation',
|
|
13
|
+
line: tok.line, column: tok.column
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# There should be at most one space before a hash rocket (=>)
|
|
4
|
+
# when there is only one parameter.
|
|
5
|
+
OpenvoxLint.new_check(:space_before_arrow) do
|
|
6
|
+
def check
|
|
7
|
+
tokens.each_with_index do |tok, i|
|
|
8
|
+
next unless tok.type == :FARROW
|
|
9
|
+
next if i == 0
|
|
10
|
+
prev = tokens[i - 1]
|
|
11
|
+
next unless prev.type == :WHITESPACE
|
|
12
|
+
next if prev.value.length <= 1
|
|
13
|
+
# Allow if this is in a multi-param aligned block
|
|
14
|
+
# (arrow_alignment handles that)
|
|
15
|
+
notify :warning,
|
|
16
|
+
message: "more than one space before => (found #{prev.value.length})",
|
|
17
|
+
line: tok.line, column: prev.column
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Use # comments, not /* */ multi-line comments.
|
|
4
|
+
OpenvoxLint.new_check(:star_comments) do
|
|
5
|
+
def check
|
|
6
|
+
tokens.each do |tok|
|
|
7
|
+
next unless tok.type == :MLCOMMENT
|
|
8
|
+
notify :warning,
|
|
9
|
+
message: 'use # (hash) comments instead of /* */ block comments',
|
|
10
|
+
line: tok.line, column: tok.column
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Indentation should use exactly 2 spaces per level.
|
|
4
|
+
OpenvoxLint.new_check(:strict_indent) do
|
|
5
|
+
def check
|
|
6
|
+
manifest_lines.each_with_index do |line, idx|
|
|
7
|
+
next if line.strip.empty?
|
|
8
|
+
next if line =~ /\A#/ # comment lines
|
|
9
|
+
indent = line.match(/\A( *)/)[1]
|
|
10
|
+
next if indent.length.even?
|
|
11
|
+
notify :warning,
|
|
12
|
+
message: "odd number of spaces in indentation (#{indent.length}); use 2-space increments",
|
|
13
|
+
line: idx + 1, column: 1
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Top-scope fact variables ($::fact_name) are deprecated in Puppet 8 / OpenVox 8.
|
|
4
|
+
# Use $facts['fact_name'] instead.
|
|
5
|
+
OpenvoxLint.new_check(:top_scope_facts) do
|
|
6
|
+
def check
|
|
7
|
+
tokens.each do |tok|
|
|
8
|
+
next unless tok.type == :VARIABLE
|
|
9
|
+
name = tok.value.sub(/^\$/, '')
|
|
10
|
+
next unless name.start_with?('::')
|
|
11
|
+
fact_name = name.sub(/\A::/, '')
|
|
12
|
+
# Skip module-qualified variables (e.g. ::mymodule::param)
|
|
13
|
+
next if fact_name.include?('::')
|
|
14
|
+
notify :warning,
|
|
15
|
+
message: "top-scope fact '$::#{fact_name}' — use $facts['#{fact_name}'] instead (Puppet 8 / OpenVox 8)",
|
|
16
|
+
line: tok.line, column: tok.column
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Resource bodies and parameter lists should end with a trailing comma.
|
|
4
|
+
OpenvoxLint.new_check(:trailing_comma) do
|
|
5
|
+
def check
|
|
6
|
+
tokens.each_with_index do |tok, i|
|
|
7
|
+
next unless tok.type == :RBRACE || tok.type == :RPAREN
|
|
8
|
+
# Find the previous non-whitespace token
|
|
9
|
+
j = i - 1
|
|
10
|
+
j -= 1 while j >= 0 && tokens[j].formatting?
|
|
11
|
+
next if j < 0
|
|
12
|
+
prev = tokens[j]
|
|
13
|
+
# Skip if previous is opening brace/paren (empty block) or already a comma
|
|
14
|
+
next if %i[LBRACE LPAREN COMMA RBRACE].include?(prev.type)
|
|
15
|
+
# Only flag inside parameter lists and resource bodies
|
|
16
|
+
next unless prev.type == :SSTRING || prev.type == :STRING || prev.type == :NAME ||
|
|
17
|
+
prev.type == :VARIABLE || prev.type == :NUMBER || prev.type == :TRUE ||
|
|
18
|
+
prev.type == :FALSE || prev.type == :CLASSREF || prev.type == :RBRACK
|
|
19
|
+
notify :warning,
|
|
20
|
+
message: 'missing trailing comma after last attribute',
|
|
21
|
+
line: prev.line, column: prev.column
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Detects trailing whitespace at the end of lines.
|
|
4
|
+
OpenvoxLint.new_check(:trailing_whitespace) do
|
|
5
|
+
def check
|
|
6
|
+
manifest_lines.each_with_index do |line, idx|
|
|
7
|
+
next unless line =~ /\s+$/
|
|
8
|
+
notify :warning,
|
|
9
|
+
message: 'trailing whitespace found',
|
|
10
|
+
line: idx + 1,
|
|
11
|
+
column: line.rstrip.length + 1
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|