openvox-lint 1.0.7 → 1.0.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d7e73afd296560df9e4d0aef103a96fd6bf42483db6ceab560e4d8fd3b73813
4
- data.tar.gz: 512c4019545161cc37a3e406b1afa3b41499bec7663624b067ee1d263346ceda
3
+ metadata.gz: abe996811c55331e6e81030b3b421c822f152e7e208767c0ba8154e8d6dfe628
4
+ data.tar.gz: bbd33e6c54ac940eb8b1a3e64ef511ef7ab6412282c12f48e3739508c3d9222c
5
5
  SHA512:
6
- metadata.gz: fb51dc826fdbbf0419c0049fcd0ccba8e5310769216bb8e04a82af6beb95f053676a91022927cc1a15f8e90345f43d972b9a3c1c24453e29b3a59800338d62f0
7
- data.tar.gz: 4a48731cf89acb78c270cffdbc97cd5273c466bb7990d0f1ee8bc4fbc74799a9f38963f6714430ecd7d1b1228a12f3176d9705c137aa850feb97de439694b70d
6
+ metadata.gz: 4b1e8359096a9156ef02eb2d18dd5af1171593d0f4c47a1957827a043212185dabd09608786c1df606257ca3b21a183978c084d289c7550f900e863367f8697b
7
+ data.tar.gz: 54bb7aba3f2afef0d6ba51f0346a8f05b7bf495a944a93af14e89144c2d28008ff09243e469b9330cc715a6200c42ea9172a83cebac57164e21088f3c9117c25
data/CHANGELOG.md CHANGED
@@ -2,6 +2,83 @@
2
2
 
3
3
  All notable changes to openvox-lint will be documented in this file.
4
4
 
5
+ ## [1.0.8] - 2026-03-04
6
+
7
+ ### Fixed
8
+
9
+ - **CLI: stale configuration on re-invocation**: The global configuration
10
+ singleton is now reset at the start of every `CLI#run`. Repeated
11
+ invocations in the same Ruby process (Vim plugins, guard, Rake loops)
12
+ no longer inherit stale `disabled_checks`, `only_checks`, or other
13
+ settings from a previous run.
14
+
15
+ - **CLI: `--config` flag ignored when local RC exists**: The `-c` /
16
+ `--config FILE` flag now takes strict priority. Previously a local
17
+ `.openvox-lint.rc` would shadow an explicit `--config` path.
18
+
19
+ - **lint:ignore / lint:endignore block suppression**: Block-style ignore
20
+ comments now work as documented. `# lint:ignore:check_name` on its
21
+ own line opens a suppression block; `# lint:endignore` closes it.
22
+ All problems on lines between the two comments are suppressed for
23
+ the named checks. Inline ignore comments (on the same line as code)
24
+ continue to work as before.
25
+
26
+ - **ignore-paths glob matching with relative/absolute prefixes**: The
27
+ `--ignore-paths` glob patterns (and the defaults `vendor/**/*.pp`,
28
+ `pkg/**/*.pp`, `spec/**/*.pp`) now match correctly regardless of
29
+ whether the file was discovered via `./vendor/foo.pp`, `vendor/foo.pp`,
30
+ or an absolute path.
31
+
32
+ - **trailing_comma false positives on non-resource braces**: The
33
+ `trailing_comma` check now only fires inside resource bodies
34
+ (`name { ... }`). It no longer produces false positives on `if`,
35
+ `unless`, `case`, class bodies, or other brace-delimited contexts
36
+ where a trailing comma is not expected.
37
+
38
+ - **arrow_alignment / space_before_arrow contradictory warnings**: The
39
+ `arrow_alignment` check now defers to `space_before_arrow` when the
40
+ misalignment in a group is caused by the longest key having extra
41
+ whitespace. This eliminates contradictory double-warnings on the
42
+ same resource block.
43
+
44
+ - **Unterminated strings and regex now reported as errors**: The lexer
45
+ now raises `OpenvoxLint::Error` when a single-quoted string,
46
+ double-quoted string, or regex literal is not terminated before
47
+ end-of-file. Previously the lexer silently produced a truncated
48
+ token covering the rest of the file, leading to wrong lint results.
49
+
50
+ - **Multi-line string tokens now report correct line number**: String
51
+ tokens that span multiple lines now record the **starting** line
52
+ number, not the ending line. Checks that flag these tokens now
53
+ point to the correct source location.
54
+
55
+ - **variables_not_enclosed mixed variable handling**: Strings containing
56
+ both enclosed (`${bar}`) and unenclosed (`$foo`) variables now
57
+ correctly flag only the unenclosed references. Previously the
58
+ entire string was skipped if any `${...}` pattern was present.
59
+
60
+ - **resource_reference_without_title_capital expanded allowlist**: The
61
+ function allowlist that prevents false positives on `name[...]`
62
+ patterns now covers 40+ Puppet built-in and stdlib functions
63
+ (`each`, `map`, `filter`, `reduce`, `lookup`, `dig`, etc.),
64
+ not just the original 5.
65
+
66
+ ### Changed
67
+
68
+ - **duplicate_params / parameter_order: removed dead code**: Both checks
69
+ contained `.formatting?` guard clauses that could never trigger
70
+ because they operate on pre-filtered semantic tokens. The dead code
71
+ has been removed for clarity.
72
+
73
+ - **Check registry duplicate warning**: `OpenvoxLint.new_check` now
74
+ emits a warning to stderr (when `OPENVOX_LINT_DEBUG` is set) if a
75
+ check name that is already registered is overwritten. This helps
76
+ detect accidental name collisions in custom check plugins.
77
+
78
+ - **Gemfile cleaned up**: Removed duplicate gem declarations that were
79
+ listed in both the Gemfile `group` block and the gemspec
80
+ `add_development_dependency` entries.
81
+
5
82
  ## [1.0.7] - 2026-02-25
6
83
 
7
84
  ### Changed
data/DOCUMENTATION.md CHANGED
@@ -46,7 +46,7 @@ the lexer token types, the plugin system, and integration guidance.
46
46
 
47
47
  | Constant | Value | Description |
48
48
  |----------|-------|-------------|
49
- | `VERSION` | `'1.0.4'` | Gem version |
49
+ | `VERSION` | `'1.0.8'` | Gem version |
50
50
 
51
51
  ### Class Methods
52
52
 
@@ -54,8 +54,9 @@ the lexer token types, the plugin system, and integration guidance.
54
54
  |--------|---------|-------------|
55
55
  | `.configuration` | `Configuration` | Global configuration singleton |
56
56
  | `.configure { \|c\| }` | `Configuration` | Yields configuration for block-style setup |
57
+ | `.reset_configuration!` | `Configuration` | Reset configuration to defaults (called by CLI) |
57
58
  | `.checks` | `Hash{Symbol => Class}` | Registry of loaded check classes |
58
- | `.new_check(name, &block)` | `Class` | Register a new check plugin |
59
+ | `.new_check(name, &block)` | `Class` | Register a new check plugin (warns on duplicates) |
59
60
 
60
61
  ### Exceptions
61
62
 
@@ -248,7 +249,6 @@ notify :warning, # or :error
248
249
  | `only_checks` | `Array<Symbol>` | `[]` | Run only these checks |
249
250
  | `disabled_checks` | `Array<Symbol>` | `[]` | Skip these checks |
250
251
  | `ignore_paths` | `Array<String>` | vendor, pkg, spec | Glob patterns to ignore |
251
- | `config_file` | `String` | `.openvox-lint.rc` | RC file path |
252
252
  | `relative` | `Boolean` | `false` | Use relative paths |
253
253
  | `column` | `Boolean` | `true` | Show column numbers |
254
254
  | `custom_log_format` | `String\|nil` | `nil` | Custom format string |
@@ -517,20 +517,20 @@ Place the check file in `lib/openvox-lint/plugins/checks/my_check.rb`.
517
517
 
518
518
  ## File Inventory
519
519
 
520
- | File | Lines | Description |
521
- |------|-------|-------------|
522
- | `bin/openvox-lint` | 7 | CLI entry point |
523
- | `lib/openvox-lint.rb` | 47 | Main module, auto-loader |
524
- | `lib/openvox-lint/version.rb` | 5 | Version constant |
525
- | `lib/openvox-lint/configuration.rb` | 59 | Configuration management |
526
- | `lib/openvox-lint/token.rb` | 38 | Token data structure |
527
- | `lib/openvox-lint/lexer.rb` | 342 | Puppet/OpenVox lexer |
528
- | `lib/openvox-lint/check_plugin.rb` | 147 | Base check class |
529
- | `lib/openvox-lint/checks.rb` | 46 | Check runner |
530
- | `lib/openvox-lint/report.rb` | 86 | Output formatters |
531
- | `lib/openvox-lint/linter.rb` | 72 | File orchestrator |
532
- | `lib/openvox-lint/cli.rb` | 87 | CLI parser |
533
- | `lib/openvox-lint/plugins/checks/*.rb` | 38 files | Check plugins |
534
- | `spec/spec_helper.rb` | 41 | Test helper |
535
- | `spec/unit/lexer_spec.rb` | 85 | Lexer tests |
536
- | `spec/unit/checks_spec.rb` | 142 | Check tests |
520
+ | File | Description |
521
+ |------|-------------|
522
+ | `bin/openvox-lint` | CLI entry point |
523
+ | `lib/openvox-lint.rb` | Main module, auto-loader |
524
+ | `lib/openvox-lint/version.rb` | Version constant |
525
+ | `lib/openvox-lint/configuration.rb` | Configuration management |
526
+ | `lib/openvox-lint/token.rb` | Token data structure |
527
+ | `lib/openvox-lint/lexer.rb` | Puppet/OpenVox lexer |
528
+ | `lib/openvox-lint/check_plugin.rb` | Base check class |
529
+ | `lib/openvox-lint/checks.rb` | Check runner with lint:ignore support |
530
+ | `lib/openvox-lint/report.rb` | Output formatters |
531
+ | `lib/openvox-lint/linter.rb` | File orchestrator |
532
+ | `lib/openvox-lint/cli.rb` | CLI parser |
533
+ | `lib/openvox-lint/plugins/checks/*.rb` | 38 built-in check plugins |
534
+ | `spec/spec_helper.rb` | Test helper |
535
+ | `spec/unit/lexer_spec.rb` | Lexer tests |
536
+ | `spec/unit/checks_spec.rb` | Check tests |
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # openvox-lint
2
2
 
3
- ![Version](https://img.shields.io/badge/version-1.0.7-blue)
3
+ [![Gem Version](https://img.shields.io/gem/v/openvox-lint)](https://rubygems.org/gems/openvox-lint)
4
+ [![Gem Downloads](https://img.shields.io/gem/dt/openvox-lint)](https://rubygems.org/gems/openvox-lint)
4
5
  ![Ruby](https://img.shields.io/badge/ruby-%E2%89%A5%202.5-red)
5
6
  ![License](https://img.shields.io/badge/license-Apache%202.0-blue)
6
7
  ![Checks](https://img.shields.io/badge/built--in%20checks-38-brightgreen)
@@ -637,6 +638,12 @@ openvox-lint/
637
638
 
638
639
  ---
639
640
 
641
+ ## Links
642
+
643
+ - **RubyGems**: [rubygems.org/gems/openvox-lint](https://rubygems.org/gems/openvox-lint)
644
+ - **GitHub**: [github.com/cvquesty/openvox-lint](https://github.com/cvquesty/openvox-lint)
645
+ - **Issues**: [github.com/cvquesty/openvox-lint/issues](https://github.com/cvquesty/openvox-lint/issues)
646
+
640
647
  ## License
641
648
 
642
649
  Apache License, Version 2.0. See [LICENSE](LICENSE) for details.
@@ -83,7 +83,8 @@ module OpenvoxLint
83
83
  return false if @ignore_comments.empty?
84
84
  line = problem[:line]
85
85
  @ignore_comments.any? do |ic|
86
- ic[:line] == line && (ic[:checks].empty? || ic[:checks].include?(problem[:check].to_s))
86
+ line >= ic[:start_line] && line <= ic[:end_line] &&
87
+ (ic[:checks].empty? || ic[:checks].include?(problem[:check].to_s))
87
88
  end
88
89
  end
89
90
 
@@ -30,17 +30,61 @@ module OpenvoxLint
30
30
 
31
31
  private
32
32
 
33
+ # Parse inline and block-style lint:ignore comments.
34
+ #
35
+ # Inline: # lint:ignore:check_name — suppresses on the same line
36
+ # Block: # lint:ignore:check_name
37
+ # ...code...
38
+ # # lint:endignore — suppresses from ignore to endignore
33
39
  def parse_ignore_comments
34
40
  results = []
41
+ open_blocks = [] # stack of { start_line:, checks: }
42
+
35
43
  @tokens.each do |tok|
36
44
  next unless tok.type == :COMMENT
37
- if tok.value =~ /lint:ignore:(.+)/
38
- results << { line: tok.line, checks: Regexp.last_match(1).strip.split(/\s*,\s*/) }
45
+
46
+ if tok.value =~ /lint:endignore/
47
+ # Close the most recent open block
48
+ block = open_blocks.pop
49
+ if block
50
+ block[:end_line] = tok.line
51
+ results << block
52
+ end
53
+ elsif tok.value =~ /lint:ignore:(.+)/
54
+ checks = Regexp.last_match(1).strip.split(/\s*,\s*/)
55
+ # If there's code on the same line before this comment, treat
56
+ # it as inline-only (same line). Otherwise open a block.
57
+ if inline_ignore?(tok)
58
+ results << { start_line: tok.line, end_line: tok.line, checks: checks }
59
+ else
60
+ open_blocks.push({ start_line: tok.line, end_line: nil, checks: checks })
61
+ end
39
62
  elsif tok.value =~ /lint:ignore\b/
40
- results << { line: tok.line, checks: [] }
63
+ if inline_ignore?(tok)
64
+ results << { start_line: tok.line, end_line: tok.line, checks: [] }
65
+ else
66
+ open_blocks.push({ start_line: tok.line, end_line: nil, checks: [] })
67
+ end
41
68
  end
42
69
  end
70
+
71
+ # Any unclosed blocks extend to end-of-file
72
+ open_blocks.each do |block|
73
+ block[:end_line] = Float::INFINITY
74
+ results << block
75
+ end
76
+
43
77
  results
44
78
  end
79
+
80
+ # An ignore comment is "inline" if there is a non-formatting token
81
+ # on the same line before it (i.e. it sits at the end of a code line).
82
+ def inline_ignore?(comment_token)
83
+ @tokens.any? do |t|
84
+ t.line == comment_token.line &&
85
+ t.column < comment_token.column &&
86
+ !t.formatting?
87
+ end
88
+ end
45
89
  end
46
90
  end
@@ -6,11 +6,12 @@ module OpenvoxLint
6
6
  # Command-line interface for openvox-lint.
7
7
  class CLI
8
8
  def initialize(args = ARGV)
9
- @args = args
10
- @config = OpenvoxLint.configuration
9
+ @args = args
11
10
  end
12
11
 
13
12
  def run
13
+ OpenvoxLint.reset_configuration!
14
+ @config = OpenvoxLint.configuration
14
15
  parse_options
15
16
  load_rc_file
16
17
  if @list_checks
@@ -41,7 +42,7 @@ module OpenvoxLint
41
42
  opts.on('--only-checks CHECKS', 'Comma-separated checks') { |c| @config.only_checks = c.split(',').map { |s| s.strip.to_sym } }
42
43
  opts.on('--ignore-paths PATHS', 'Comma-separated globs') { |p| @config.ignore_paths = p.split(',').map(&:strip) }
43
44
  opts.on('--list-checks', 'List available checks') { @list_checks = true }
44
- opts.on('-c', '--config FILE', 'Config file path') { |f| @config.config_file = f }
45
+ opts.on('-c', '--config FILE', 'Config file path') { |f| @explicit_config_file = f }
45
46
  end
46
47
  remaining = []
47
48
  begin
@@ -59,7 +60,15 @@ module OpenvoxLint
59
60
  end
60
61
 
61
62
  def load_rc_file
62
- ['.openvox-lint.rc', @config.config_file, File.expand_path('~/.openvox-lint.rc')].each do |path|
63
+ # Priority: explicit --config flag > local .openvox-lint.rc > ~/.openvox-lint.rc
64
+ # An explicit --config flag must always win. The default value of
65
+ # config_file is nil until the user passes -c / --config.
66
+ candidates = if @explicit_config_file
67
+ [@explicit_config_file]
68
+ else
69
+ ['.openvox-lint.rc', File.expand_path('~/.openvox-lint.rc')]
70
+ end
71
+ candidates.each do |path|
63
72
  next unless path && File.exist?(path)
64
73
  @config.load_from_rc(path); break
65
74
  end
@@ -7,13 +7,12 @@ module OpenvoxLint
7
7
  log_format: 'text', with_filename: true, fail_on_warnings: false,
8
8
  fix: false, only_checks: [], disabled_checks: [],
9
9
  ignore_paths: %w[vendor/**/*.pp pkg/**/*.pp spec/**/*.pp],
10
- config_file: '.openvox-lint.rc', relative: false, column: true,
11
- custom_log_format: nil,
10
+ relative: false, column: true, custom_log_format: nil,
12
11
  }.freeze
13
12
 
14
13
  attr_accessor :log_format, :with_filename, :fail_on_warnings,
15
14
  :fix, :only_checks, :disabled_checks, :ignore_paths,
16
- :config_file, :relative, :column, :custom_log_format
15
+ :relative, :column, :custom_log_format
17
16
 
18
17
  def initialize
19
18
  DEFAULTS.each do |k, v|
@@ -140,8 +140,9 @@ module OpenvoxLint
140
140
  end
141
141
 
142
142
  def scan_regex
143
- start = @pos; start_col = @column
143
+ start = @pos; start_line = @line; start_col = @column
144
144
  @pos += 1; @column += 1
145
+ terminated = false
145
146
  while @pos < @code.length && @code[@pos] != '/'
146
147
  if @code[@pos] == '\\'
147
148
  @pos += 2; @column += 2
@@ -149,31 +150,41 @@ module OpenvoxLint
149
150
  @pos += 1; @column += 1
150
151
  end
151
152
  end
152
- @pos += 1; @column += 1
153
- add_token(:REGEX, @code[start...@pos], @line, start_col)
153
+ if @pos < @code.length
154
+ @pos += 1; @column += 1; terminated = true
155
+ end
156
+ if !terminated
157
+ raise OpenvoxLint::Error, "unterminated regex starting at line #{start_line}"
158
+ end
159
+ add_token(:REGEX, @code[start...@pos], start_line, start_col)
154
160
  end
155
161
 
156
162
  def scan_single_quoted_string
157
- start = @pos; start_col = @column
163
+ start = @pos; start_line = @line; start_col = @column
158
164
  @pos += 1; @column += 1
165
+ terminated = false
159
166
  while @pos < @code.length
160
167
  if @code[@pos] == '\\'
161
168
  @pos += 2; @column += 2
162
169
  elsif @code[@pos] == "'"
163
- @pos += 1; @column += 1; break
170
+ @pos += 1; @column += 1; terminated = true; break
164
171
  elsif @code[@pos] == "\n"
165
172
  @line += 1; @column = 1; @pos += 1
166
173
  else
167
174
  @pos += 1; @column += 1
168
175
  end
169
176
  end
170
- add_token(:SSTRING, @code[start...@pos], @line, start_col)
177
+ if !terminated
178
+ raise OpenvoxLint::Error, "unterminated single-quoted string starting at line #{start_line}"
179
+ end
180
+ add_token(:SSTRING, @code[start...@pos], start_line, start_col)
171
181
  end
172
182
 
173
183
  def scan_double_quoted_string
174
- start = @pos; start_col = @column
184
+ start = @pos; start_line = @line; start_col = @column
175
185
  @pos += 1; @column += 1
176
186
  has_interp = false
187
+ terminated = false
177
188
  while @pos < @code.length
178
189
  if @code[@pos] == '\\'
179
190
  @pos += 2; @column += 2
@@ -184,15 +195,18 @@ module OpenvoxLint
184
195
  @pos += 1; @column += 1
185
196
  end
186
197
  elsif @code[@pos] == '"'
187
- @pos += 1; @column += 1; break
198
+ @pos += 1; @column += 1; terminated = true; break
188
199
  elsif @code[@pos] == "\n"
189
200
  @line += 1; @column = 1; @pos += 1
190
201
  else
191
202
  @pos += 1; @column += 1
192
203
  end
193
204
  end
205
+ if !terminated
206
+ raise OpenvoxLint::Error, "unterminated double-quoted string starting at line #{start_line}"
207
+ end
194
208
  type = has_interp ? :DQSTRING : :STRING
195
- add_token(type, @code[start...@pos], @line, start_col)
209
+ add_token(type, @code[start...@pos], start_line, start_col)
196
210
  end
197
211
 
198
212
  def scan_variable
@@ -64,8 +64,14 @@ module OpenvoxLint
64
64
  end
65
65
 
66
66
  def ignored?(filepath)
67
+ # Normalize away leading './' so that patterns like 'vendor/**/*.pp'
68
+ # match regardless of whether the file was discovered as
69
+ # './vendor/foo.pp' or 'vendor/foo.pp'.
70
+ normalized = filepath.sub(%r{\A\./}, '')
67
71
  @config.ignore_paths.any? do |pat|
68
- File.fnmatch?(pat, filepath, File::FNM_PATHNAME | File::FNM_DOTMATCH)
72
+ File.fnmatch?(pat, normalized, File::FNM_PATHNAME | File::FNM_DOTMATCH) ||
73
+ File.fnmatch?(pat, filepath, File::FNM_PATHNAME | File::FNM_DOTMATCH) ||
74
+ File.fnmatch?("**/#{pat}", normalized, File::FNM_PATHNAME | File::FNM_DOTMATCH)
69
75
  end
70
76
  end
71
77
  end
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Hash rockets (=>) should be aligned within a resource body.
4
+ #
5
+ # Only fires on groups of 2+ arrows. Skips groups where the
6
+ # misalignment is caused by the longest key having extra leading
7
+ # whitespace — that case is already reported by the
8
+ # space_before_arrow check to avoid duplicate / contradictory advice.
4
9
  OpenvoxLint.new_check(:arrow_alignment) do
5
10
  def check
6
11
  # Group FARROW tokens by resource (consecutive lines)
@@ -11,8 +16,16 @@ OpenvoxLint.new_check(:arrow_alignment) do
11
16
  groups = group_arrows(arrows)
12
17
  groups.each do |group|
13
18
  next if group.size <= 1
19
+
20
+ # If the arrows are already at the same column, nothing to do.
14
21
  columns = group.map(&:column)
15
22
  max_col = columns.max
23
+ next if columns.all? { |c| c == max_col }
24
+
25
+ # Skip this group if the misalignment is caused by extra space
26
+ # before the longest-key arrow — space_before_arrow handles that.
27
+ next if longest_key_has_extra_space?(group)
28
+
16
29
  group.each do |arrow|
17
30
  next if arrow.column == max_col
18
31
  notify :warning,
@@ -39,4 +52,29 @@ OpenvoxLint.new_check(:arrow_alignment) do
39
52
  groups << current unless current.empty?
40
53
  groups
41
54
  end
55
+
56
+ # Returns true when the arrow belonging to the longest key in the
57
+ # group has more than one space before it. In that situation the
58
+ # space_before_arrow check will fire, so arrow_alignment should stay
59
+ # silent to avoid contradictory advice.
60
+ def longest_key_has_extra_space?(group)
61
+ group.each do |arrow|
62
+ idx = tokens.index(arrow)
63
+ next unless idx && idx >= 2
64
+ ws = tokens[idx - 1]
65
+ next unless ws.type == :WHITESPACE
66
+ key = tokens[idx - 2]
67
+ next if key.formatting?
68
+ # Find the longest key length in the group
69
+ max_klen = group.map { |a|
70
+ ai = tokens.index(a)
71
+ next 0 unless ai && ai >= 2 && tokens[ai - 1].type == :WHITESPACE
72
+ tokens[ai - 2].value.length
73
+ }.max
74
+ if key.value.length == max_klen && ws.value.length > 1
75
+ return true
76
+ end
77
+ end
78
+ false
79
+ end
42
80
  end
@@ -8,9 +8,9 @@ OpenvoxLint.new_check(:duplicate_params) do
8
8
  params = res[:param_tokens]
9
9
  params.each_with_index do |tok, i|
10
10
  next unless tok.type == :NAME
11
- # Check if followed by =>
11
+ # param_tokens are already semantic (non-formatting), so the
12
+ # next token is directly adjacent.
12
13
  j = i + 1
13
- j += 1 while j < params.length && params[j].formatting?
14
14
  next unless j < params.length && params[j].type == :FARROW
15
15
  if seen[tok.value]
16
16
  notify :error,
@@ -37,7 +37,9 @@ OpenvoxLint.new_check(:legacy_facts) do
37
37
  def check
38
38
  tokens.each do |tok|
39
39
  next unless tok.type == :VARIABLE
40
- name = tok.value.sub(/^\$/, '').sub(/\A::/, '')
40
+ # Strip leading $ and :: prefix, and any trailing : left by the
41
+ # lexer when a variable is used as a resource title ($fact:).
42
+ name = tok.value.sub(/^\$/, '').sub(/\A::/, '').chomp(':')
41
43
  next unless LEGACY_FACTS.include?(name)
42
44
  notify :warning,
43
45
  message: "legacy fact '#{name}' — use $facts['...'] structured fact instead (Puppet 8 / OpenVox 8)",
@@ -8,9 +8,9 @@ OpenvoxLint.new_check(:parameter_order) do
8
8
  found_default = false
9
9
  params.each_with_index do |tok, i|
10
10
  next unless tok.type == :VARIABLE
11
- # Look ahead for = (default value)
11
+ # param_tokens from find_keyword_indexes are already semantic
12
+ # (non-formatting), so the next token is directly adjacent.
12
13
  j = i + 1
13
- j += 1 while j < params.length && params[j].formatting?
14
14
  has_default = j < params.length && params[j].type == :EQUALS
15
15
  if has_default
16
16
  found_default = true
@@ -3,6 +3,21 @@
3
3
  # Resource reference titles must start with a capital letter.
4
4
  # e.g. File['/tmp/foo'] not file['/tmp/foo']
5
5
  OpenvoxLint.new_check(:resource_reference_without_title_capital) do
6
+ # Puppet built-in functions and common stdlib functions that accept
7
+ # bracket/index syntax (name[...]). These are NOT resource
8
+ # references and must not be flagged.
9
+ FUNCTION_ALLOWLIST = %w[
10
+ split join include contain require
11
+ each map filter reduce select reject
12
+ slice flatten any all empty
13
+ assert_type create_resources defined dig
14
+ ensure_packages ensure_resource epp fail
15
+ fqdn_rand generate hiera hiera_array hiera_hash hiera_include
16
+ inline_epp inline_template lookup match md5 notice
17
+ realize regsubst sha1 sprintf tag template unique versioncmp
18
+ with
19
+ ].freeze
20
+
6
21
  def check
7
22
  sem = semantic_tokens
8
23
  sem.each_with_index do |tok, i|
@@ -11,8 +26,8 @@ OpenvoxLint.new_check(:resource_reference_without_title_capital) do
11
26
  next unless sem[i + 1].type == :LBRACK
12
27
  # This is name[...] which should be Name[...] for a resource reference
13
28
  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)
29
+ # Skip function calls, method calls, and normal array access
30
+ next if FUNCTION_ALLOWLIST.include?(tok.value)
16
31
  notify :warning,
17
32
  message: "resource reference '#{tok.value}' must start with a capital letter",
18
33
  line: tok.line, column: tok.column
@@ -6,7 +6,9 @@ OpenvoxLint.new_check(:top_scope_facts) do
6
6
  def check
7
7
  tokens.each do |tok|
8
8
  next unless tok.type == :VARIABLE
9
- name = tok.value.sub(/^\$/, '')
9
+ # Strip leading $ and any trailing : left by the lexer when a
10
+ # variable is used as a resource title ($::fact:).
11
+ name = tok.value.sub(/^\$/, '').chomp(':')
10
12
  next unless name.start_with?('::')
11
13
  fact_name = name.sub(/\A::/, '')
12
14
  # Skip module-qualified variables (e.g. ::mymodule::param)
@@ -1,24 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Resource bodies and parameter lists should end with a trailing comma.
4
+ #
5
+ # Only fires inside resource bodies (NAME { ... }) — not inside
6
+ # conditionals (if/unless/case), class bodies, or other brace contexts
7
+ # where a trailing comma is not expected.
4
8
  OpenvoxLint.new_check(:trailing_comma) do
5
9
  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
10
+ resource_indexes.each do |res|
11
+ params = res[:param_tokens]
12
+ next if params.empty?
13
+
14
+ # Find the last non-formatting token in the resource body
15
+ last = params.reverse_each.find { |t| !t.formatting? }
16
+ next unless last
17
+
18
+ # Already has a trailing comma — nothing to do
19
+ next if last.type == :COMMA
20
+
21
+ # Only flag after value-like tokens (not structural tokens)
22
+ next unless %i[SSTRING STRING NAME VARIABLE NUMBER TRUE FALSE
23
+ CLASSREF RBRACK DQSTRING].include?(last.type)
24
+
19
25
  notify :warning,
20
26
  message: 'missing trailing comma after last attribute',
21
- line: prev.line, column: prev.column
27
+ line: last.line, column: last.column
22
28
  end
23
29
  end
24
30
  end
@@ -2,18 +2,24 @@
2
2
 
3
3
  # Variables in double-quoted strings should be enclosed in braces.
4
4
  # e.g. "$foo" should be "${foo}"
5
+ #
6
+ # Correctly handles strings with a mix of enclosed and unenclosed
7
+ # variables — e.g. "$foo and ${bar}" flags the unenclosed $foo even
8
+ # though ${bar} is properly enclosed.
5
9
  OpenvoxLint.new_check(:variables_not_enclosed) do
6
10
  def check
7
11
  tokens.each do |tok|
8
12
  next unless tok.type == :DQSTRING || tok.type == :STRING
9
13
  val = tok.value
10
- # Look for $var not followed by { inside double-quoted strings
11
- next unless val =~ /\$([a-zA-Z_][a-zA-Z0-9_:]*)/
12
- next if val =~ /\$\{/ # already enclosed
13
- notify :warning,
14
- message: 'variable not enclosed in braces within string',
15
- line: tok.line,
16
- column: tok.column
14
+ # Scan for any $var that is NOT preceded by ${ (already enclosed).
15
+ # We use a negative lookbehind to skip ${...} patterns and only
16
+ # match bare $varname references.
17
+ val.scan(/(?<!\$)\$(?!\{)([a-zA-Z_][a-zA-Z0-9_:]*)/) do
18
+ notify :warning,
19
+ message: 'variable not enclosed in braces within string',
20
+ line: tok.line,
21
+ column: tok.column
22
+ end
17
23
  end
18
24
  end
19
25
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenvoxLint
4
- VERSION = '1.0.7'
4
+ VERSION = '1.0.8'
5
5
  end
data/lib/openvox-lint.rb CHANGED
@@ -29,11 +29,21 @@ module OpenvoxLint
29
29
  yield(configuration)
30
30
  end
31
31
 
32
+ # Reset the global configuration singleton. Called at the start of
33
+ # every CLI run so that repeated invocations in the same Ruby
34
+ # process (Vim plugins, guard, Rake loops) start clean.
35
+ def reset_configuration!
36
+ @configuration = Configuration.new
37
+ end
38
+
32
39
  def checks
33
40
  @checks ||= {}
34
41
  end
35
42
 
36
43
  def new_check(name, &block)
44
+ if checks.key?(name)
45
+ $stderr.puts "openvox-lint: warning: check '#{name}' is already registered — overwriting" if ENV['OPENVOX_LINT_DEBUG']
46
+ end
37
47
  klass = Class.new(CheckPlugin, &block)
38
48
  klass.instance_variable_set(:@check_name, name)
39
49
  checks[name] = klass
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openvox-lint
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.7
4
+ version: 1.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jerald Sheets
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-27 00:00:00.000000000 Z
11
+ date: 2026-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -126,7 +126,7 @@ metadata:
126
126
  bug_tracker_uri: https://github.com/cvquesty/openvox-lint/issues
127
127
  changelog_uri: https://github.com/cvquesty/openvox-lint/blob/main/CHANGELOG.md
128
128
  rubygems_mfa_required: 'true'
129
- post_install_message:
129
+ post_install_message:
130
130
  rdoc_options: []
131
131
  require_paths:
132
132
  - lib
@@ -141,8 +141,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
141
  - !ruby/object:Gem::Version
142
142
  version: '0'
143
143
  requirements: []
144
- rubygems_version: 3.0.3.1
145
- signing_key:
144
+ rubygems_version: 3.4.19
145
+ signing_key:
146
146
  specification_version: 4
147
147
  summary: Check OpenVox/Puppet manifests against the style guide
148
148
  test_files: []