openvox-lint 1.0.7 → 1.3.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.
data/README.md CHANGED
@@ -1,14 +1,15 @@
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
- ![Checks](https://img.shields.io/badge/built--in%20checks-38-brightgreen)
7
+ ![Checks](https://img.shields.io/badge/built--in%20checks-37-brightgreen)
7
8
  ![Status](https://img.shields.io/badge/status-stable-brightgreen)
8
9
 
9
10
  **A style-guide linter for OpenVox and Puppet manifests.**
10
11
 
11
- openvox-lint checks your `.pp` manifest files against the [Puppet Style Guide](https://puppet.com/docs/puppet/latest/style_guide.html) and catches common errors, deprecated patterns, legacy facts, strict-mode violations, and Puppet 8+ / OpenVox 8.x language issues.
12
+ openvox-lint checks your `.pp` manifest files against the [OpenVox Language Style Guide](https://docs.openvoxproject.org/openvox/latest/style_guide.html) (and Puppet equivalent) and catches common errors, deprecated patterns, legacy facts, strict-mode violations, and Puppet 8+ / OpenVox 8.x language issues.
12
13
 
13
14
  Fully compatible with:
14
15
  - **OpenVox 8.x** (the community-maintained open-source fork of Puppet)
@@ -24,7 +25,7 @@ Fully compatible with:
24
25
  - [Quick Start](#quick-start)
25
26
  - [Usage](#usage)
26
27
  - [Enabling and Disabling Checks](#enabling-and-disabling-checks)
27
- - [Built-in Checks (38)](#built-in-checks)
28
+ - [Built-in Checks (37)](#built-in-checks)
28
29
  - [Configuration](#configuration)
29
30
  - [Output Formats](#output-formats)
30
31
  - [Integration](#integration)
@@ -32,6 +33,7 @@ Fully compatible with:
32
33
  - [Updating](#updating)
33
34
  - [Uninstalling](#uninstalling)
34
35
  - [Development](#development)
36
+ - [Contributors](#contributors)
35
37
  - [License](#license)
36
38
 
37
39
  ---
@@ -158,6 +160,27 @@ openvox-lint -f github manifests/
158
160
  openvox-lint --fail-on-warnings manifests/
159
161
  ```
160
162
 
163
+ ### Fix mode (`--fix`)
164
+
165
+ `--fix` automatically corrects problems for the checks that implement fix logic. Fixes are applied in-place and are safe for the supported cases.
166
+
167
+ Currently supported checks:
168
+ - `trailing_whitespace` — removes trailing spaces/tabs
169
+ - `hard_tabs` — converts tabs to two spaces
170
+ - `quoted_booleans` — changes `'true'` / `'false'` (strings) to unquoted booleans
171
+ - `double_quoted_strings` — converts unnecessary double-quoted strings (no interpolation or escapes) to single quotes
172
+ - `single_quote_string_with_variables` — converts single-quoted strings containing `$variables` to double quotes so interpolation works
173
+
174
+ Example:
175
+
176
+ ```bash
177
+ openvox-lint --fix manifests/
178
+ # or
179
+ openvox-lint --fix .
180
+ ```
181
+
182
+ After running, re-lint to confirm. Complex structural fixes (e.g. arrow alignment) are not yet implemented.
183
+
161
184
  ---
162
185
 
163
186
  ## Enabling and Disabling Checks
@@ -181,7 +204,7 @@ openvox-lint --no-line_length-check --no-arrow_alignment-check --no-strict_inden
181
204
  Use `--only-checks` to run *only* the checks you specify (everything else is skipped):
182
205
 
183
206
  ```bash
184
- # Only check for legacy facts and hiera3 usage
207
+ # Only check for legacy facts and deprecated Hiera 3 functions
185
208
  openvox-lint --only-checks legacy_facts,hiera3_function,top_scope_facts manifests/
186
209
 
187
210
  # Only check for errors (skip all warnings)
@@ -231,13 +254,13 @@ To see every check that openvox-lint can run:
231
254
  openvox-lint --list-checks
232
255
  ```
233
256
 
234
- This shows the check name, severity (warning or error), and description for all 38 built-in checks.
257
+ This shows the check name, severity (warning or error), and description for all 37 built-in checks.
235
258
 
236
259
  ---
237
260
 
238
261
  ## Built-in Checks
239
262
 
240
- openvox-lint ships with **38 built-in checks** organized into categories:
263
+ openvox-lint ships with **37 built-in checks** organized into categories:
241
264
 
242
265
  ### Whitespace & Formatting (5 checks)
243
266
 
@@ -301,13 +324,12 @@ openvox-lint ships with **38 built-in checks** organized into categories:
301
324
  | `case_without_default` | warning | Case statements must have a `default` case |
302
325
  | `selector_inside_resource` | warning | Don't use selectors inside resource bodies |
303
326
 
304
- ### References & Syntax (4 checks)
327
+ ### References & Syntax (3 checks)
305
328
 
306
329
  | Check | Severity | Description |
307
330
  |-------|----------|-------------|
308
331
  | `leading_zero` | warning | No leading zeros in numbers (except file modes) |
309
332
  | `resource_reference_without_title_capital` | warning | Capitalise resource reference types |
310
- | `relative_classname_inclusion` | warning | Use fully qualified class names |
311
333
  | `autoloader_layout` | warning | Class/define name must match file path |
312
334
 
313
335
  ### Comments (1 check)
@@ -334,7 +356,7 @@ openvox-lint ships with **38 built-in checks** organized into categories:
334
356
  |-------|----------|-------------|
335
357
  | `legacy_facts` | warning | Legacy facts removed in Puppet 8; use `$facts['...']` |
336
358
  | `top_scope_facts` | warning | `$::fact` references; use `$facts['fact']` |
337
- | `hiera3_function` | **error** | `hiera()` / `hiera_hash()` removed; use `lookup()` |
359
+ | `hiera3_function` | **error** | Deprecated Hiera 3 functions removed; use Hiera 5 `lookup()` |
338
360
  | `import_statement` | **error** | `import` removed in Puppet 4+ |
339
361
 
340
362
  ---
@@ -381,7 +403,7 @@ class foo { # lint:ignore:documentation
381
403
 
382
404
  ```
383
405
  manifests/init.pp:5:15: WARNING: unquoted_file_mode: unquoted file mode
384
- manifests/init.pp:9:3: ERROR: hiera3_function: 'hiera()' is removed in Puppet 8
406
+ manifests/init.pp:9:3: ERROR: hiera3_function: deprecated Hiera 3 function 'hiera()' — use Hiera 5 lookup() instead
385
407
  ```
386
408
 
387
409
  ### JSON (`-f json`)
@@ -616,14 +638,14 @@ openvox-lint/
616
638
 
617
639
  ## Comparison with puppet-lint
618
640
 
619
- | Feature | puppet-lint 5.x | openvox-lint 1.0 |
641
+ | Feature | puppet-lint 5.x | openvox-lint 1.3.0 |
620
642
  |---------|-----------------|------------------|
621
643
  | Ruby requirement | ≥ 3.1 | ≥ 2.5 (RHEL 8, macOS, all modern platforms) |
622
644
  | Runtime dependencies | None | None |
623
- | Built-in checks | ~25 | 38 |
645
+ | Built-in checks | ~25 | 37 |
624
646
  | Legacy facts detection | Via plugin | Built-in |
625
647
  | Top-scope facts detection | Via plugin | Built-in |
626
- | Hiera 3 detection | No | Built-in (error) |
648
+ | Deprecated Hiera 3 function detection | No | Built-in (error) |
627
649
  | Import statement detection | No | Built-in (error) |
628
650
  | Strict indent check | Via plugin | Built-in |
629
651
  | GitHub Actions output | No | Built-in (`-f github`) |
@@ -632,17 +654,33 @@ openvox-lint/
632
654
  | Custom log format | Yes | Yes (compatible) |
633
655
  | OpenVox awareness | No | Yes |
634
656
  | Plugin system | Yes | Yes (compatible) |
635
- | `--fix` support | Yes | Yes |
657
+ | `--fix` support | Yes | Yes (real implementations for trailing_whitespace, hard_tabs, quoted_booleans, double_quoted_strings, single_quote_string_with_variables + line-based) |
636
658
  | vim-openvox integration | No | Native |
637
659
 
638
660
  ---
639
661
 
662
+ ## Links
663
+
664
+ - **RubyGems**: [rubygems.org/gems/openvox-lint](https://rubygems.org/gems/openvox-lint)
665
+ - **GitHub**: [github.com/cvquesty/openvox-lint](https://github.com/cvquesty/openvox-lint)
666
+ - **Issues**: [github.com/cvquesty/openvox-lint/issues](https://github.com/cvquesty/openvox-lint/issues)
667
+
640
668
  ## License
641
669
 
642
670
  Apache License, Version 2.0. See [LICENSE](LICENSE) for details.
643
671
 
644
672
  ---
645
673
 
674
+ ## Contributors
675
+
676
+ Thanks to the following people for their contributions to openvox-lint:
677
+
678
+ | Contributor | Contribution |
679
+ |-------------|--------------|
680
+ | **[@hbro](https://github.com/hbro)** (Hans Broeckx) | Fixed `legacy_facts` false positives on local variables ([PR #1](https://github.com/cvquesty/openvox-lint/pull/1)) — *First community contribution!* 🎉 |
681
+
682
+ Want to contribute? See [CONTRIBUTING.md](CONTRIBUTING.md) to get started!
683
+
646
684
  ## Author
647
685
 
648
686
  Jerald Sheets ([@cvquesty](https://github.com/cvquesty))
@@ -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
@@ -42,6 +42,24 @@ module OpenvoxLint
42
42
  fullpath: filepath, configuration: @config,
43
43
  )
44
44
  @problems.concat(checker.run)
45
+
46
+ # Apply --fix: checks' #fix methods (if implemented) mutate the shared
47
+ # manifest_lines array in place. After all checks, write the (possibly
48
+ # fixed) content back if it differs. This makes --fix functional for
49
+ # supported checks. Ensures final newline per style guide.
50
+ if @config.fix
51
+ begin
52
+ fixed_code = lexer.manifest_lines.join("\n") + "\n"
53
+ if fixed_code != code
54
+ File.write(filepath, fixed_code)
55
+ end
56
+ rescue StandardError => write_err
57
+ @problems << {
58
+ path: filepath, line: 0, column: 0, kind: :error,
59
+ check: :fix, message: "Failed to write fixes: #{write_err.message}",
60
+ }
61
+ end
62
+ end
45
63
  rescue StandardError => e
46
64
  @problems << {
47
65
  path: filepath, line: 0, column: 0, kind: :error,
@@ -64,8 +82,14 @@ module OpenvoxLint
64
82
  end
65
83
 
66
84
  def ignored?(filepath)
85
+ # Normalize away leading './' so that patterns like 'vendor/**/*.pp'
86
+ # match regardless of whether the file was discovered as
87
+ # './vendor/foo.pp' or 'vendor/foo.pp'.
88
+ normalized = filepath.sub(%r{\A\./}, '')
67
89
  @config.ignore_paths.any? do |pat|
68
- File.fnmatch?(pat, filepath, File::FNM_PATHNAME | File::FNM_DOTMATCH)
90
+ File.fnmatch?(pat, normalized, File::FNM_PATHNAME | File::FNM_DOTMATCH) ||
91
+ File.fnmatch?(pat, filepath, File::FNM_PATHNAME | File::FNM_DOTMATCH) ||
92
+ File.fnmatch?("**/#{pat}", normalized, File::FNM_PATHNAME | File::FNM_DOTMATCH)
69
93
  end
70
94
  end
71
95
  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
@@ -24,4 +24,20 @@ OpenvoxLint.new_check(:double_quoted_strings) do
24
24
  column: tok.column
25
25
  end
26
26
  end
27
+
28
+ # --fix support: convert the double-quoted string (no interp) to single
29
+ # quotes by swapping the outer delimiters at the reported column.
30
+ def fix(problem)
31
+ idx = problem[:line] - 1
32
+ line = @manifest_lines[idx].dup
33
+ col = (problem[:column] || 1) - 1
34
+ if line[col] == '"'
35
+ end_col = line.index('"', col + 1)
36
+ if end_col
37
+ inner = line[(col + 1)...end_col]
38
+ line[col..end_col] = "'" + inner + "'"
39
+ @manifest_lines[idx] = line
40
+ end
41
+ end
42
+ end
27
43
  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,
@@ -12,4 +12,11 @@ OpenvoxLint.new_check(:hard_tabs) do
12
12
  column: col + 1
13
13
  end
14
14
  end
15
+
16
+ # --fix support: replace all hard tabs with two spaces (simple but effective
17
+ # for consistent soft-tab style; more sophisticated indent fixers exist).
18
+ def fix(problem)
19
+ idx = problem[:line] - 1
20
+ @manifest_lines[idx] = @manifest_lines[idx].gsub("\t", ' ')
21
+ end
15
22
  end
@@ -1,7 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Hiera 3 functions (hiera, hiera_array, hiera_hash, hiera_include)
4
- # are removed in Puppet 8 / OpenVox 8. Use lookup() instead.
3
+ # Detects deprecated Hiera 3 functions that are removed in Puppet 8 / OpenVox 8.
4
+ #
5
+ # Hiera 3 is fully deprecated. Only Hiera 5 is supported in Puppet 8+.
6
+ # The legacy hiera() functions were compatibility shims that have been removed.
7
+ #
8
+ # Deprecated functions:
9
+ # - hiera() -> lookup()
10
+ # - hiera_array() -> lookup(..., Array, 'unique')
11
+ # - hiera_hash() -> lookup(..., Hash, 'hash')
12
+ # - hiera_include() -> lookup(...).include
13
+ #
14
+ # Use the Hiera 5 lookup() function instead.
5
15
  OpenvoxLint.new_check(:hiera3_function) do
6
16
  HIERA3_FUNCS = %w[hiera hiera_array hiera_hash hiera_include].freeze
7
17
 
@@ -9,7 +19,7 @@ OpenvoxLint.new_check(:hiera3_function) do
9
19
  tokens.each do |tok|
10
20
  next unless tok.type == :NAME && HIERA3_FUNCS.include?(tok.value)
11
21
  notify :error,
12
- message: "'#{tok.value}()' is removed in Puppet 8 / OpenVox 8 — use lookup() instead",
22
+ message: "deprecated Hiera 3 function '#{tok.value}()' — use Hiera 5 lookup() instead",
13
23
  line: tok.line, column: tok.column
14
24
  end
15
25
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'set'
4
+
3
5
  # Legacy (unstructured) top-scope facts are removed in Puppet 8 / OpenVox 8.
4
6
  # Use structured facts via $facts hash instead.
5
7
  #
@@ -35,13 +37,57 @@ OpenvoxLint.new_check(:legacy_facts) do
35
37
  ].freeze
36
38
 
37
39
  def check
40
+ local_vars = collect_local_vars
38
41
  tokens.each do |tok|
39
42
  next unless tok.type == :VARIABLE
40
- name = tok.value.sub(/^\$/, '').sub(/\A::/, '')
43
+ # Strip leading $ and :: prefix, and any trailing : left by the
44
+ # lexer when a variable is used as a resource title ($fact:).
45
+ name = tok.value.sub(/^\$/, '').sub(/\A::/, '').chomp(':')
41
46
  next unless LEGACY_FACTS.include?(name)
47
+ next if local_vars.include?(name)
42
48
  notify :warning,
43
49
  message: "legacy fact '#{name}' — use $facts['...'] structured fact instead (Puppet 8 / OpenVox 8)",
44
50
  line: tok.line, column: tok.column
45
51
  end
46
52
  end
53
+
54
+ private
55
+
56
+ # Collect variable names that are declared locally (class/define parameters
57
+ # or lambda block parameters) so they are not mistaken for legacy facts.
58
+ def collect_local_vars
59
+ local = Set.new
60
+ toks = tokens
61
+ i = 0
62
+ while i < toks.size
63
+ case toks[i].type
64
+ when :CLASS, :DEFINE
65
+ # Skip to the opening LPAREN of the parameter list (stop at LBRACE)
66
+ j = i + 1
67
+ j += 1 while j < toks.size && toks[j].type != :LPAREN && toks[j].type != :LBRACE
68
+ if j < toks.size && toks[j].type == :LPAREN
69
+ depth = 1
70
+ j += 1
71
+ while j < toks.size && depth > 0
72
+ case toks[j].type
73
+ when :LPAREN then depth += 1
74
+ when :RPAREN then depth -= 1
75
+ when :VARIABLE then local << toks[j].value.sub(/^\$/, '').sub(/\A::/, '') if depth == 1
76
+ end
77
+ j += 1
78
+ end
79
+ end
80
+ when :PIPE
81
+ # Lambda parameter list: |$var1, $var2|
82
+ j = i + 1
83
+ while j < toks.size && toks[j].type != :PIPE
84
+ local << toks[j].value.sub(/^\$/, '').sub(/\A::/, '') if toks[j].type == :VARIABLE
85
+ j += 1
86
+ end
87
+ i = j # skip past closing PIPE
88
+ end
89
+ i += 1
90
+ end
91
+ local
92
+ end
47
93
  end
@@ -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
@@ -13,4 +13,25 @@ OpenvoxLint.new_check(:quoted_booleans) do
13
13
  line: tok.line, column: tok.column
14
14
  end
15
15
  end
16
+
17
+ # --fix support: unquote the boolean at the reported column (or fallback
18
+ # gsub). Assumes the problem points to the start of the quoted value.
19
+ def fix(problem)
20
+ idx = problem[:line] - 1
21
+ line = @manifest_lines[idx].dup
22
+ col = (problem[:column] || 1) - 1
23
+ if line[col] == "'" || line[col] == '"'
24
+ quote = line[col]
25
+ # Find matching closing quote for this simple boolean token
26
+ end_col = line.index(quote, col + 1)
27
+ if end_col && %w[true false].include?(line[(col + 1)...end_col])
28
+ val = line[(col + 1)...end_col]
29
+ line[col..end_col] = val
30
+ @manifest_lines[idx] = line
31
+ return
32
+ end
33
+ end
34
+ # Fallback (safe for most manifests)
35
+ @manifest_lines[idx] = line.gsub(/(['"])(true|false)\1/, '\2')
36
+ end
16
37
  end