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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +52 -0
  3. data/DOCUMENTATION.md +481 -0
  4. data/LICENSE +83 -0
  5. data/README.md +497 -0
  6. data/bin/openvox-lint +7 -0
  7. data/lib/openvox-lint/check_plugin.rb +147 -0
  8. data/lib/openvox-lint/checks.rb +46 -0
  9. data/lib/openvox-lint/cli.rb +87 -0
  10. data/lib/openvox-lint/configuration.rb +59 -0
  11. data/lib/openvox-lint/lexer.rb +342 -0
  12. data/lib/openvox-lint/linter.rb +72 -0
  13. data/lib/openvox-lint/plugins/checks/arrow_alignment.rb +42 -0
  14. data/lib/openvox-lint/plugins/checks/autoloader_layout.rb +31 -0
  15. data/lib/openvox-lint/plugins/checks/case_without_default.rb +28 -0
  16. data/lib/openvox-lint/plugins/checks/class_inherits_params.rb +13 -0
  17. data/lib/openvox-lint/plugins/checks/documentation.rb +26 -0
  18. data/lib/openvox-lint/plugins/checks/double_quoted_strings.rb +19 -0
  19. data/lib/openvox-lint/plugins/checks/duplicate_params.rb +24 -0
  20. data/lib/openvox-lint/plugins/checks/ensure_first_param.rb +28 -0
  21. data/lib/openvox-lint/plugins/checks/ensure_not_symlink_target.rb +29 -0
  22. data/lib/openvox-lint/plugins/checks/file_mode.rb +33 -0
  23. data/lib/openvox-lint/plugins/checks/hard_tabs.rb +15 -0
  24. data/lib/openvox-lint/plugins/checks/hiera3_function.rb +16 -0
  25. data/lib/openvox-lint/plugins/checks/import_statement.rb +13 -0
  26. data/lib/openvox-lint/plugins/checks/inherits_across_namespaces.rb +27 -0
  27. data/lib/openvox-lint/plugins/checks/leading_zero.rb +22 -0
  28. data/lib/openvox-lint/plugins/checks/legacy_facts.rb +47 -0
  29. data/lib/openvox-lint/plugins/checks/line_length.rb +18 -0
  30. data/lib/openvox-lint/plugins/checks/nested_classes_or_defines.rb +26 -0
  31. data/lib/openvox-lint/plugins/checks/node_name_unquoted.rb +18 -0
  32. data/lib/openvox-lint/plugins/checks/only_variable_string.rb +18 -0
  33. data/lib/openvox-lint/plugins/checks/parameter_order.rb +25 -0
  34. data/lib/openvox-lint/plugins/checks/puppet_url_without_modules.rb +17 -0
  35. data/lib/openvox-lint/plugins/checks/quoted_booleans.rb +16 -0
  36. data/lib/openvox-lint/plugins/checks/relative_classname_inclusion.rb +24 -0
  37. data/lib/openvox-lint/plugins/checks/resource_reference_without_title_capital.rb +21 -0
  38. data/lib/openvox-lint/plugins/checks/selector_inside_resource.rb +15 -0
  39. data/lib/openvox-lint/plugins/checks/single_quote_string_with_variables.rb +16 -0
  40. data/lib/openvox-lint/plugins/checks/space_before_arrow.rb +20 -0
  41. data/lib/openvox-lint/plugins/checks/star_comments.rb +13 -0
  42. data/lib/openvox-lint/plugins/checks/strict_indent.rb +16 -0
  43. data/lib/openvox-lint/plugins/checks/top_scope_facts.rb +19 -0
  44. data/lib/openvox-lint/plugins/checks/trailing_comma.rb +24 -0
  45. data/lib/openvox-lint/plugins/checks/trailing_whitespace.rb +14 -0
  46. data/lib/openvox-lint/plugins/checks/unquoted_file_mode.rb +24 -0
  47. data/lib/openvox-lint/plugins/checks/unquoted_resource_title.rb +13 -0
  48. data/lib/openvox-lint/plugins/checks/variable_contains_dash.rb +15 -0
  49. data/lib/openvox-lint/plugins/checks/variable_is_lowercase.rb +16 -0
  50. data/lib/openvox-lint/plugins/checks/variables_not_enclosed.rb +19 -0
  51. data/lib/openvox-lint/report.rb +86 -0
  52. data/lib/openvox-lint/token.rb +38 -0
  53. data/lib/openvox-lint/version.rb +5 -0
  54. data/lib/openvox-lint.rb +47 -0
  55. 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