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/DOCUMENTATION.md CHANGED
@@ -10,6 +10,42 @@ reported in multiple output formats suitable for humans, CI systems, and IDEs.
10
10
  This document covers the architecture, every public API, every built-in check,
11
11
  the lexer token types, the plugin system, and integration guidance.
12
12
 
13
+ **Version:** 1.3.0
14
+ **Checks:** 37 built-in (with real --fix support for 5+ checks)
15
+ **License:** Apache 2.0
16
+ **Compatibility:** OpenVox 8.x, Puppet 8.x, Puppet 7.x (with deprecation warnings)
17
+
18
+ ---
19
+
20
+ ## Table of Contents
21
+
22
+ - [Architecture](#architecture)
23
+ - [API Reference](#api-reference)
24
+ - [Module: OpenvoxLint](#module-openvoxlint)
25
+ - [Class: Token](#class-openvoxlinttoken)
26
+ - [Class: Lexer](#class-openvoxlintlexer)
27
+ - [Class: CheckPlugin](#class-openvoxlintcheckplugin)
28
+ - [Class: Configuration](#class-openvoxlintconfiguration)
29
+ - [Class: Linter](#class-openvoxlintlinter)
30
+ - [Class: Report](#class-openvoxlintreport)
31
+ - [Complete Check Reference (37 Checks)](#complete-check-reference-37-checks)
32
+ - [Whitespace & Formatting (5)](#whitespace--formatting-5-checks)
33
+ - [Arrow Alignment (1)](#arrow-alignment-1-check)
34
+ - [Quoting & Strings (5)](#quoting--strings-5-checks)
35
+ - [Variables (2)](#variables-2-checks)
36
+ - [Resources (7)](#resources-7-checks)
37
+ - [Classes & Defines (5)](#classes--defines-5-checks)
38
+ - [Conditionals (2)](#conditionals-2-checks)
39
+ - [References & Syntax (3)](#references--syntax-3-checks)
40
+ - [Comments (1)](#comments-1-check)
41
+ - [URLs (1)](#urls-1-check)
42
+ - [Nodes (1)](#nodes-1-check)
43
+ - [Puppet 8 / OpenVox 8 Compatibility (4)](#puppet-8--openvox-8-compatibility-4-checks)
44
+ - [Token Types Reference](#token-types-reference)
45
+ - [Plugin Development](#plugin-development)
46
+ - [Migration from puppet-lint](#migration-from-puppet-lint)
47
+ - [File Inventory](#file-inventory)
48
+
13
49
  ---
14
50
 
15
51
  ## Architecture
@@ -34,197 +70,166 @@ the lexer token types, the plugin system, and integration guidance.
34
70
 
35
71
  1. **CLI** parses command-line arguments and loads configuration
36
72
  2. **Linter** expands file arguments, reads each `.pp` file
37
- 3. **Lexer** tokenises the manifest into `Token` objects
73
+ 3. **Lexer** tokenises the manifest into `Token` objects (doubly-linked list)
38
74
  4. **Checks** runs each enabled check plugin against the token stream
39
75
  5. **Report** formats and outputs the collected problems
40
76
 
77
+ ### Design Principles
78
+
79
+ - **Zero runtime dependencies** — only Ruby standard library
80
+ - **Token-based analysis** — works on token stream, not AST
81
+ - **Puppet/OpenVox agnostic** — identical language support for both
82
+ - **Extensible** — plugin system for custom checks
83
+ - **CI-friendly** — multiple output formats, proper exit codes
84
+
41
85
  ---
42
86
 
43
- ## Module: OpenvoxLint
87
+ ## API Reference
88
+
89
+ ### Module: OpenvoxLint
90
+
91
+ The top-level namespace for all openvox-lint classes.
44
92
 
45
- ### Constants
93
+ #### Constants
46
94
 
47
95
  | Constant | Value | Description |
48
96
  |----------|-------|-------------|
49
- | `VERSION` | `'1.0.4'` | Gem version |
97
+ | `VERSION` | `'1.3.0'` | Gem version string |
50
98
 
51
- ### Class Methods
99
+ #### Class Methods
52
100
 
53
101
  | Method | Returns | Description |
54
102
  |--------|---------|-------------|
55
103
  | `.configuration` | `Configuration` | Global configuration singleton |
56
104
  | `.configure { \|c\| }` | `Configuration` | Yields configuration for block-style setup |
105
+ | `.reset_configuration!` | `Configuration` | Reset configuration to defaults (called by CLI) |
57
106
  | `.checks` | `Hash{Symbol => Class}` | Registry of loaded check classes |
58
- | `.new_check(name, &block)` | `Class` | Register a new check plugin |
107
+ | `.new_check(name, &block)` | `Class` | Register a new check plugin (warns on duplicates) |
59
108
 
60
- ### Exceptions
109
+ #### Exceptions
61
110
 
62
111
  | Exception | Inherits | Usage |
63
112
  |-----------|----------|-------|
64
- | `OpenvoxLint::Error` | `StandardError` | General errors |
65
- | `OpenvoxLint::NoFix` | `StandardError` | Raised to skip fixing a problem |
113
+ | `OpenvoxLint::Error` | `StandardError` | General errors (syntax, unterminated strings) |
114
+ | `OpenvoxLint::NoFix` | `StandardError` | Raised by `fix()` to skip unfixable problems |
115
+
116
+ #### Example Usage
117
+
118
+ ```ruby
119
+ require 'openvox-lint'
120
+
121
+ # Configure globally
122
+ OpenvoxLint.configure do |c|
123
+ c.fail_on_warnings = true
124
+ c.ignore_paths = ['vendor/**/*.pp']
125
+ end
126
+
127
+ # Access check registry
128
+ OpenvoxLint.checks.keys # => [:trailing_whitespace, :legacy_facts, ...]
129
+ ```
66
130
 
67
131
  ---
68
132
 
69
- ## Class: OpenvoxLint::Token
133
+ ### Class: OpenvoxLint::Token
70
134
 
71
- Represents a single token from the lexer.
135
+ Represents a single token produced by the lexer. Tokens are linked in a
136
+ doubly-linked list for easy forward/backward navigation during checks.
72
137
 
73
- ### Attributes
138
+ #### Attributes
74
139
 
75
140
  | Attribute | Type | Description |
76
141
  |-----------|------|-------------|
77
- | `type` | `Symbol` | Token type (see Token Types below) |
78
- | `value` | `String` | Raw text value |
142
+ | `type` | `Symbol` | Token type (see Token Types Reference) |
143
+ | `value` | `String` | Raw text value including quotes for strings |
79
144
  | `line` | `Integer` | 1-based line number |
80
145
  | `column` | `Integer` | 1-based column number |
81
146
  | `prev_token` | `Token\|nil` | Previous token in doubly-linked list |
82
147
  | `next_token` | `Token\|nil` | Next token in doubly-linked list |
83
148
 
84
- ### Methods
149
+ #### Methods
85
150
 
86
151
  | Method | Returns | Description |
87
152
  |--------|---------|-------------|
88
- | `#formatting?` | `Boolean` | True if whitespace/comment/indent/newline |
153
+ | `#formatting?` | `Boolean` | True if whitespace, indent, newline, or comment |
89
154
  | `#to_s` | `String` | Human-readable representation |
155
+ | `#inspect` | `String` | Debug representation |
90
156
 
91
- ### Token Types
92
-
93
- #### Keywords
94
-
95
- | Type | Puppet Keyword |
96
- |------|---------------|
97
- | `:AND` | `and` |
98
- | `:APPLICATION` | `application` |
99
- | `:ATTR` | `attr` |
100
- | `:CASE` | `case` |
101
- | `:CLASS` | `class` |
102
- | `:CONSUMES` | `consumes` |
103
- | `:DEFAULT` | `default` |
104
- | `:DEFINE` | `define` |
105
- | `:ELSE` | `else` |
106
- | `:ELSIF` | `elsif` |
107
- | `:FALSE` | `false` |
108
- | `:FUNCTION` | `function` |
109
- | `:IF` | `if` |
110
- | `:IMPORT` | `import` |
111
- | `:IN` | `in` |
112
- | `:INHERITS` | `inherits` |
113
- | `:NODE` | `node` |
114
- | `:NOT` | `not` |
115
- | `:OR` | `or` |
116
- | `:PRIVATE` | `private` |
117
- | `:PRODUCES` | `produces` |
118
- | `:SITE` | `site` |
119
- | `:TRUE` | `true` |
120
- | `:TYPE` | `type` |
121
- | `:UNDEF` | `undef` |
122
- | `:UNLESS` | `unless` |
123
-
124
- #### Identifiers & Literals
125
-
126
- | Type | Description | Example |
127
- |------|-------------|---------|
128
- | `:NAME` | Identifier / bare word | `ensure`, `myclass` |
129
- | `:CLASSREF` | Capitalised reference | `File`, `String`, `Stdlib::Absolutepath` |
130
- | `:VARIABLE` | Variable | `$foo`, `$::bar::baz` |
131
- | `:NUMBER` | Numeric literal | `42`, `0xFF`, `3.14` |
132
- | `:SSTRING` | Single-quoted string | `'hello'` |
133
- | `:STRING` | Double-quoted string (no interpolation) | `"hello"` |
134
- | `:DQSTRING` | Double-quoted string (with interpolation) | `"hello ${name}"` |
135
- | `:REGEX` | Regular expression | `/^foo/` |
136
- | `:HEREDOC_OPEN` | Heredoc opening tag | `@("END")` |
137
- | `:HEREDOC` | Heredoc body | content |
138
-
139
- #### Operators
140
-
141
- | Type | Operator | Type | Operator |
142
- |------|----------|------|----------|
143
- | `:FARROW` | `=>` | `:PARROW` | `+>` |
144
- | `:ISEQUAL` | `==` | `:NOTEQUAL` | `!=` |
145
- | `:MATCH` | `=~` | `:NOMATCH` | `!~` |
146
- | `:LESSEQUAL` | `<=` | `:GREATEREQUAL` | `>=` |
147
- | `:LESSTHAN` | `<` | `:GREATERTHAN` | `>` |
148
- | `:LSHIFT` | `<<` | `:RSHIFT` | `>>` |
149
- | `:IN_EDGE` | `->` | `:OUT_EDGE` | `<-` |
150
- | `:IN_EDGE_SUB` | `~>` | `:OUT_EDGE_SUB` | `<~` |
151
- | `:APPENDS` | `+=` | `:EQUALS` | `=` |
152
- | `:LCOLLECT` | `<\|` | `:RCOLLECT` | `\|>` |
153
- | `:LLCOLLECT` | `<<\|` | `:RRCOLLECT` | `\|>>` |
154
-
155
- #### Punctuation
156
-
157
- | Type | Character | Type | Character |
158
- |------|-----------|------|-----------|
159
- | `:LBRACE` | `{` | `:RBRACE` | `}` |
160
- | `:LPAREN` | `(` | `:RPAREN` | `)` |
161
- | `:LBRACK` | `[` | `:RBRACK` | `]` |
162
- | `:COMMA` | `,` | `:SEMIC` | `;` |
163
- | `:DOT` | `.` | `:COLON` | `:` |
164
- | `:PIPE` | `\|` | `:AT` | `@` |
165
- | `:QMARK` | `?` | `:BACKSLASH` | `\\` |
166
- | `:PLUS` | `+` | `:MINUS` | `-` |
167
- | `:TIMES` | `*` | `:MODULO` | `%` |
168
- | `:DIV` | `/` | `:NOT` | `!` |
157
+ #### Formatting Token Types
169
158
 
170
- #### Formatting
159
+ The following types return `true` for `#formatting?`:
171
160
 
172
- | Type | Description |
173
- |------|-------------|
174
- | `:WHITESPACE` | Spaces/tabs (not at line start) |
175
- | `:INDENT` | Spaces/tabs at line start |
176
- | `:NEWLINE` | Line break |
177
- | `:COMMENT` | `#` comment |
178
- | `:MLCOMMENT` | `/* */` comment |
179
- | `:SLASH_COMMENT` | `//` comment |
161
+ - `:WHITESPACE` — spaces/tabs not at line start
162
+ - `:INDENT` — spaces/tabs at line start
163
+ - `:NEWLINE` — line breaks
164
+ - `:COMMENT` — `#` comments
165
+ - `:MLCOMMENT` — `/* */` block comments
166
+ - `:SLASH_COMMENT` — `//` comments
180
167
 
181
168
  ---
182
169
 
183
- ## Class: OpenvoxLint::Lexer
170
+ ### Class: OpenvoxLint::Lexer
171
+
172
+ Tokenises a Puppet/OpenVox manifest string into an array of Token objects.
173
+ Recognises all Puppet 8 / OpenVox 8.x language constructs.
184
174
 
185
- ### Constructor
175
+ #### Constructor
186
176
 
187
177
  ```ruby
188
178
  lexer = OpenvoxLint::Lexer.new(code_string)
189
179
  ```
190
180
 
191
- ### Attributes
181
+ Raises `OpenvoxLint::Error` for unterminated strings, regex, or heredocs.
182
+
183
+ #### Attributes
192
184
 
193
185
  | Attribute | Type | Description |
194
186
  |-----------|------|-------------|
195
187
  | `tokens` | `Array<Token>` | All tokens (doubly-linked) |
196
188
  | `manifest_lines` | `Array<String>` | Source lines (for line-based checks) |
197
189
 
190
+ #### Supported Constructs
191
+
192
+ - All Puppet keywords (class, define, if, case, etc.)
193
+ - Variables (`$foo`, `$::bar::baz`)
194
+ - Single and double-quoted strings with escape handling
195
+ - String interpolation (`"Hello ${name}"`)
196
+ - Heredocs (`@("END")`)
197
+ - Regular expressions (`/pattern/`)
198
+ - All operators (`=>`, `->`, `~>`, `==`, etc.)
199
+ - Class references (`File`, `String`, `Stdlib::Absolutepath`)
200
+ - Numbers (decimal, hex, octal, float, scientific)
201
+ - Comments (`#`, `/* */`, `//`)
202
+
198
203
  ---
199
204
 
200
- ## Class: OpenvoxLint::CheckPlugin
205
+ ### Class: OpenvoxLint::CheckPlugin
201
206
 
202
- Base class for all checks. Created via `OpenvoxLint.new_check`.
207
+ Base class for all lint checks. Create new checks via `OpenvoxLint.new_check`.
203
208
 
204
- ### Subclass Interface
209
+ #### Subclass Interface
205
210
 
206
211
  | Method | Required | Description |
207
212
  |--------|----------|-------------|
208
- | `#check` | **Yes** | Main check logic; call `notify` to report |
213
+ | `#check` | **Yes** | Main check logic; call `notify` to report problems |
209
214
  | `#fix(problem)` | No | Auto-fix a problem; raise `NoFix` to skip |
210
215
 
211
- ### Helper Methods Available in Checks
216
+ #### Helper Methods Available in Checks
212
217
 
213
218
  | Method | Returns | Description |
214
219
  |--------|---------|-------------|
215
220
  | `tokens` | `Array<Token>` | Full token stream |
216
- | `manifest_lines` | `Array<String>` | Source lines |
221
+ | `manifest_lines` | `Array<String>` | Source lines (0-indexed) |
217
222
  | `semantic_tokens` | `Array<Token>` | Non-formatting tokens only |
218
- | `resource_indexes` | `Array<Hash>` | Resource body locations |
223
+ | `resource_indexes` | `Array<Hash>` | Resource body locations with param_tokens |
219
224
  | `class_indexes` | `Array<Hash>` | Class definition locations |
220
225
  | `defined_type_indexes` | `Array<Hash>` | Defined type locations |
221
226
  | `node_indexes` | `Array<Hash>` | Node definition locations |
222
227
  | `title_tokens` | `Array<Token>` | Resource title tokens |
223
- | `fullpath` | `String` | Full file path |
228
+ | `fullpath` | `String` | Full file path being checked |
224
229
  | `filename` | `String` | Base filename |
225
230
  | `notify(kind, details)` | — | Report a problem |
226
231
 
227
- ### `notify` Parameters
232
+ #### `notify` Parameters
228
233
 
229
234
  ```ruby
230
235
  notify :warning, # or :error
@@ -233,11 +238,24 @@ notify :warning, # or :error
233
238
  column: 5
234
239
  ```
235
240
 
241
+ #### Resource Index Structure
242
+
243
+ ```ruby
244
+ {
245
+ type: Token, # Resource type token (e.g., "file")
246
+ start: Integer, # Semantic token index of opening brace
247
+ end: Integer, # Semantic token index of closing brace
248
+ param_tokens: Array # Tokens inside the resource body
249
+ }
250
+ ```
251
+
236
252
  ---
237
253
 
238
- ## Class: OpenvoxLint::Configuration
254
+ ### Class: OpenvoxLint::Configuration
239
255
 
240
- ### Attributes
256
+ Holds all runtime configuration for a lint session.
257
+
258
+ #### Attributes
241
259
 
242
260
  | Attribute | Type | Default | Description |
243
261
  |-----------|------|---------|-------------|
@@ -247,34 +265,43 @@ notify :warning, # or :error
247
265
  | `fix` | `Boolean` | `false` | Auto-fix mode |
248
266
  | `only_checks` | `Array<Symbol>` | `[]` | Run only these checks |
249
267
  | `disabled_checks` | `Array<Symbol>` | `[]` | Skip these checks |
250
- | `ignore_paths` | `Array<String>` | vendor, pkg, spec | Glob patterns to ignore |
251
- | `config_file` | `String` | `.openvox-lint.rc` | RC file path |
252
- | `relative` | `Boolean` | `false` | Use relative paths |
268
+ | `ignore_paths` | `Array<String>` | `['vendor/**/*.pp', 'pkg/**/*.pp', 'spec/**/*.pp']` | Glob patterns to ignore |
269
+ | `relative` | `Boolean` | `false` | Use relative paths in output |
253
270
  | `column` | `Boolean` | `true` | Show column numbers |
254
271
  | `custom_log_format` | `String\|nil` | `nil` | Custom format string |
255
272
 
256
- ### Methods
273
+ #### Methods
257
274
 
258
275
  | Method | Description |
259
276
  |--------|-------------|
260
277
  | `#load_from_rc(path)` | Load config from an RC file |
261
- | `#check_enabled?(name)` | Is a check enabled? |
278
+ | `#check_enabled?(name)` | Returns true if check is enabled |
279
+
280
+ #### RC File Format
281
+
282
+ ```
283
+ # .openvox-lint.rc
284
+ # Each line is a command-line flag
285
+
286
+ --no-line_length-check
287
+ --no-documentation-check
288
+ --fail-on-warnings
289
+ --ignore-paths vendor/**/*.pp,pkg/**/*.pp
290
+ ```
262
291
 
263
292
  ---
264
293
 
265
- ## Class: OpenvoxLint::Linter
294
+ ### Class: OpenvoxLint::Linter
266
295
 
267
- ### Usage
296
+ Orchestrates the linting of one or more manifest files.
297
+
298
+ #### Constructor
268
299
 
269
300
  ```ruby
270
- linter = OpenvoxLint::Linter.new
271
- linter.run('manifests/')
272
- linter.problems # => Array of problem hashes
273
- linter.errors? # => true/false
274
- linter.exit_code # => 0 or 1
301
+ linter = OpenvoxLint::Linter.new(configuration: config)
275
302
  ```
276
303
 
277
- ### Methods
304
+ #### Methods
278
305
 
279
306
  | Method | Returns | Description |
280
307
  |--------|---------|-------------|
@@ -285,50 +312,186 @@ linter.exit_code # => 0 or 1
285
312
  | `#warnings?` | `Boolean` | Any warnings found? |
286
313
  | `#exit_code` | `Integer` | 0=clean, 1=problems |
287
314
 
315
+ #### Problem Hash Structure
316
+
317
+ ```ruby
318
+ {
319
+ path: 'manifests/init.pp',
320
+ line: 42,
321
+ column: 5,
322
+ kind: :warning, # or :error
323
+ check: :legacy_facts,
324
+ message: "legacy fact 'osfamily' — use $facts['...']"
325
+ }
326
+ ```
327
+
328
+ #### Usage Example
329
+
330
+ ```ruby
331
+ require 'openvox-lint'
332
+
333
+ config = OpenvoxLint::Configuration.new
334
+ config.fail_on_warnings = true
335
+ config.only_checks = [:legacy_facts, :hiera3_function]
336
+
337
+ linter = OpenvoxLint::Linter.new(configuration: config)
338
+ linter.run('manifests/')
339
+
340
+ puts "Checked #{linter.file_count} files"
341
+ puts "Found #{linter.problems.size} problems"
342
+ exit linter.exit_code
343
+ ```
344
+
288
345
  ---
289
346
 
290
- ## Class: OpenvoxLint::Report
347
+ ### Class: OpenvoxLint::Report
291
348
 
292
- ### Usage
349
+ Formats and outputs lint problems in various formats.
350
+
351
+ #### Constructor
293
352
 
294
353
  ```ruby
295
- report = OpenvoxLint::Report.new(config)
296
- report.format(problems) # to $stdout
297
- report.format(problems, io: file) # to file
354
+ report = OpenvoxLint::Report.new(configuration)
298
355
  ```
299
356
 
300
- ### Supported Formats
357
+ #### Methods
358
+
359
+ | Method | Description |
360
+ |--------|-------------|
361
+ | `#format(problems, io: $stdout)` | Format and output problems |
362
+
363
+ #### Supported Formats
301
364
 
302
365
  | Format | Flag | Description |
303
366
  |--------|------|-------------|
304
367
  | `text` | `-f text` (default) | `path:line:col: KIND: check: message` |
305
- | `json` | `-f json` | JSON array |
368
+ | `json` | `-f json` | JSON array of problem objects |
306
369
  | `csv` | `-f csv` | CSV with headers |
307
370
  | `github` | `-f github` | GitHub Actions annotations |
308
- | `codeclimate` | `-f codeclimate` | Code Climate JSON |
371
+ | `codeclimate` | `-f codeclimate` | Code Climate JSON format |
309
372
  | `custom` | `--log-format` | User-defined format string |
310
373
 
374
+ #### Custom Format Placeholders
375
+
376
+ | Placeholder | Value |
377
+ |-------------|-------|
378
+ | `%{path}` | File path |
379
+ | `%{line}` | Line number |
380
+ | `%{column}` | Column number |
381
+ | `%{KIND}` | Uppercase severity (WARNING/ERROR) |
382
+ | `%{kind}` | Lowercase severity |
383
+ | `%{check}` | Check name |
384
+ | `%{message}` | Problem message |
385
+
386
+ ---
387
+
388
+ ## Complete Check Reference (37 Checks)
389
+
390
+ ### Whitespace & Formatting (5 checks)
391
+
392
+ #### `trailing_whitespace` (WARNING)
393
+
394
+ Detects trailing whitespace at the end of lines.
395
+
396
+ **Why:** Trailing whitespace is invisible noise that pollutes diffs and can cause
397
+ merge conflicts.
398
+
399
+ **Bad:**
400
+ ```puppet
401
+ class foo {
402
+ ensure => present,
403
+ }
404
+ ```
405
+
406
+ **Good:**
407
+ ```puppet
408
+ class foo {
409
+ ensure => present,
410
+ }
411
+ ```
412
+
311
413
  ---
312
414
 
313
- ## Complete Check Reference
415
+ #### `hard_tabs` (WARNING)
416
+
417
+ Detects hard tab characters. Puppet style requires 2-space soft tabs.
418
+
419
+ **Why:** Tabs render inconsistently across editors and terminals. The Puppet
420
+ Style Guide mandates 2-space indentation.
421
+
422
+ **Bad:**
423
+ ```puppet
424
+ class foo {
425
+ ensure => present,
426
+ }
427
+ ```
428
+
429
+ **Good:**
430
+ ```puppet
431
+ class foo {
432
+ ensure => present,
433
+ }
434
+ ```
435
+
436
+ ---
437
+
438
+ #### `line_length` (WARNING)
439
+
440
+ Lines should not exceed 140 characters.
441
+
442
+ **Why:** Long lines are hard to read, especially in code review and side-by-side
443
+ diffs. The check has a built-in exception for long `puppet:///` URLs which
444
+ often cannot be broken.
445
+
446
+ **Bad:**
447
+ ```puppet
448
+ file { '/etc/config': content => 'This is a very long string that exceeds the maximum line length limit and makes the code hard to read in most editors and code review tools' }
449
+ ```
450
+
451
+ **Good:**
452
+ ```puppet
453
+ $content = 'This is a long string that has been assigned to a variable'
454
+
455
+ file { '/etc/config':
456
+ content => $content,
457
+ }
458
+ ```
459
+
460
+ ---
461
+
462
+ #### `strict_indent` (WARNING)
463
+
464
+ Indentation must use 2-space increments. Flags lines with odd numbers of
465
+ leading spaces.
466
+
467
+ **Why:** Consistent indentation improves readability. Puppet convention is
468
+ exactly 2 spaces per nesting level.
314
469
 
315
- ### Whitespace & Alignment Checks
470
+ **Bad:**
471
+ ```puppet
472
+ class foo {
473
+ ensure => present, # 3 spaces - odd!
474
+ }
475
+ ```
476
+
477
+ **Good:**
478
+ ```puppet
479
+ class foo {
480
+ ensure => present, # 2 spaces
481
+ }
482
+ ```
483
+
484
+ ---
316
485
 
317
486
  #### `space_before_arrow` (WARNING)
318
487
 
319
- Controls spacing before `=>` (hash rocket) in resource parameter blocks.
320
- In Puppet manifests, it is standard practice to vertically align `=>`
321
- arrows within a resource body. This means the parameter with the
322
- **longest key name** has exactly one space before `=>`, and all shorter
323
- keys have additional padding spaces to bring their `=>` into alignment.
488
+ In aligned parameter blocks, only the longest key should have exactly one space
489
+ before `=>`. Extra spaces on the longest key indicate over-padding.
324
490
 
325
- The check groups `=>` tokens by line proximity. Within each group it
326
- identifies the longest key and only flags that key if it has more than
327
- one space before `=>`. Shorter keys are permitted extra spaces for
328
- alignment. A single-parameter resource with extra space before `=>`
329
- is always flagged (nothing to align with).
491
+ **Why:** When arrows are aligned, shorter keys need padding. But the longest
492
+ key sets the alignment column and should have exactly one space.
330
493
 
331
- **Good — properly aligned (no warnings):**
494
+ **Good — properly aligned:**
332
495
  ```puppet
333
496
  file { '/etc/nginx/nginx.conf':
334
497
  ensure => file,
@@ -339,21 +502,18 @@ file { '/etc/nginx/nginx.conf':
339
502
  }
340
503
  ```
341
504
 
342
- Here `content` is the longest key (7 characters). It has a single space
343
- before `=>`. All other keys (`ensure`, `owner`, `group`, `mode`) have
344
- padding to align their `=>` with `content =>`'s column. No warnings.
505
+ Here `content` (7 chars) is the longest key with 1 space before `=>`.
506
+ Other keys have padding spaces for alignment.
345
507
 
346
508
  **Bad — longest key has extra space:**
347
509
  ```puppet
348
510
  file { '/tmp/foo':
349
511
  ensure => present,
350
512
  mode => '0644',
351
- owner => 'root',
352
513
  }
353
514
  ```
354
515
 
355
- `ensure` is the longest key (6 chars) but has 2 spaces before `=>`.
356
- The check flags `ensure` only; `mode` and `owner` padding is fine.
516
+ `ensure` is longest (6 chars) but has 2 spaces before `=>`.
357
517
 
358
518
  **Bad — single parameter with extra space:**
359
519
  ```puppet
@@ -362,175 +522,1148 @@ package { 'httpd':
362
522
  }
363
523
  ```
364
524
 
365
- Only one parameter — no alignment context — the 3 extra spaces are
366
- flagged.
525
+ One parameter means nothing to align with — extra spaces flagged.
367
526
 
368
527
  ---
369
528
 
370
- ### Puppet 8 / OpenVox 8 Migration Checks
529
+ ### Arrow Alignment (1 check)
371
530
 
372
- These are the most important checks for users upgrading from Puppet 7 or
373
- migrating to OpenVox.
531
+ #### `arrow_alignment` (WARNING)
374
532
 
375
- #### `legacy_facts` (WARNING)
533
+ Hash rockets (`=>`) should be aligned within a resource body. This check
534
+ groups arrows by line proximity and flags any that are not at the maximum
535
+ column position for that group.
376
536
 
377
- Legacy (unstructured) top-scope facts are excluded by default in Puppet 8 /
378
- OpenVox 8. Variables like `$osfamily`, `$fqdn`, `$ipaddress`, and
379
- `$operatingsystem` must be replaced with structured facts.
537
+ **Why:** Vertical alignment improves readability of resource declarations.
538
+ It makes it easy to scan parameter values at a glance.
539
+
540
+ **Note:** This check coordinates with `space_before_arrow` to avoid
541
+ contradictory warnings. If misalignment is caused by extra spaces before
542
+ the longest key's arrow, only `space_before_arrow` fires.
543
+
544
+ **Bad — misaligned arrows:**
545
+ ```puppet
546
+ file { '/tmp/foo':
547
+ ensure => file,
548
+ content => 'hello',
549
+ mode => '0644',
550
+ }
551
+ ```
552
+
553
+ **Good — aligned arrows:**
554
+ ```puppet
555
+ file { '/tmp/foo':
556
+ ensure => file,
557
+ content => 'hello',
558
+ mode => '0644',
559
+ }
560
+ ```
561
+
562
+ ---
563
+
564
+ ### Quoting & Strings (5 checks)
565
+
566
+ #### `double_quoted_strings` (WARNING)
567
+
568
+ Double-quoted strings that contain no variables or escape sequences should
569
+ use single quotes instead.
570
+
571
+ **Exception:** If the string contains literal single-quote characters
572
+ (`'`), double quotes are correct to avoid escaping.
573
+
574
+ **Why:** Single quotes signal "this is a literal string" while double quotes
575
+ signal "this may contain interpolation". Using single quotes when possible
576
+ makes intent clearer.
380
577
 
381
578
  **Bad:**
382
579
  ```puppet
383
- if $osfamily == 'RedHat' { }
580
+ file { "/tmp/foo":
581
+ ensure => "present",
582
+ }
384
583
  ```
385
584
 
386
585
  **Good:**
387
586
  ```puppet
388
- if $facts['os']['family'] == 'RedHat' { }
587
+ file { '/tmp/foo':
588
+ ensure => 'present',
589
+ }
590
+ ```
591
+
592
+ **Exception — nested single quotes (allowed):**
593
+ ```puppet
594
+ notify { "It's working":
595
+ message => "Use 'ensure' as first parameter",
596
+ }
389
597
  ```
390
598
 
391
- Covers 80+ legacy fact names.
599
+ ---
600
+
601
+ #### `only_variable_string` (WARNING)
392
602
 
393
- #### `top_scope_facts` (WARNING)
603
+ A string containing only a variable should not be quoted.
394
604
 
395
- Top-scope fact variables (`$::hostname`) should use the `$facts` hash.
605
+ **Why:** `"${foo}"` is semantically identical to `$foo` but adds visual noise
606
+ and suggests interpolation where none occurs.
396
607
 
397
608
  **Bad:**
398
609
  ```puppet
399
- $hostname = $::hostname
610
+ $result = "${some_variable}"
611
+ file { "${path}": }
400
612
  ```
401
613
 
402
614
  **Good:**
403
615
  ```puppet
404
- $hostname = $facts['networking']['hostname']
616
+ $result = $some_variable
617
+ file { $path: }
405
618
  ```
406
619
 
407
- #### `hiera3_function` (ERROR)
620
+ ---
621
+
622
+ #### `single_quote_string_with_variables` (WARNING)
623
+
624
+ Single-quoted strings containing `$variable` patterns should use double
625
+ quotes for interpolation.
408
626
 
409
- Hiera 3 functions are removed in Puppet 8. This is an error, not a warning.
627
+ **Why:** Variables in single-quoted strings are literal text, not interpolated.
628
+ This is usually a mistake.
410
629
 
411
630
  **Bad:**
412
631
  ```puppet
413
- $val = hiera('mykey')
414
- $hash = hiera_hash('myhash')
632
+ notify { 'hello':
633
+ message => 'Hello $name, welcome!', # $name is literal
634
+ }
415
635
  ```
416
636
 
417
637
  **Good:**
418
638
  ```puppet
419
- $val = lookup('mykey')
420
- $hash = lookup('myhash', Hash, 'hash')
639
+ notify { 'hello':
640
+ message => "Hello ${name}, welcome!", # $name is interpolated
641
+ }
421
642
  ```
422
643
 
423
- #### `import_statement` (ERROR)
644
+ ---
424
645
 
425
- The `import` keyword was removed in Puppet 4.
646
+ #### `variables_not_enclosed` (WARNING)
647
+
648
+ Variables in double-quoted strings should be enclosed in braces (`${var}`).
649
+
650
+ **Why:** Brace syntax makes variable boundaries explicit and allows array/hash
651
+ access. `$foo` works but `${foo}` is clearer, especially with adjacent text.
426
652
 
427
653
  **Bad:**
428
654
  ```puppet
429
- import 'foo'
655
+ $msg = "Hello $name!"
656
+ $path = "/home/$user/bin"
430
657
  ```
431
658
 
432
659
  **Good:**
433
- Use module autoloading.
660
+ ```puppet
661
+ $msg = "Hello ${name}!"
662
+ $path = "/home/${user}/bin"
663
+ ```
434
664
 
435
- ---
665
+ **Mixed handling:** A string with both enclosed and unenclosed variables
666
+ correctly flags only the unenclosed ones:
436
667
 
437
- ## Puppet 8 / OpenVox 8 Language Context
668
+ ```puppet
669
+ $msg = "Hello $name, your home is ${home}" # Only $name flagged
670
+ ```
438
671
 
439
- openvox-lint is designed with full awareness of the Puppet 8 / OpenVox 8
440
- language changes:
672
+ ---
441
673
 
442
- ### Changes from Puppet 7
674
+ #### `quoted_booleans` (WARNING)
443
675
 
444
- | Change | Impact | openvox-lint Check |
445
- |--------|--------|-------------------|
446
- | Strict mode enabled by default | Undefined vars → errors | (runtime) |
447
- | Legacy facts excluded | `$osfamily` etc. unavailable | `legacy_facts` |
448
- | Top-scope facts deprecated | `$::fact` pattern obsolete | `top_scope_facts` |
449
- | Hiera 3 removed | `hiera()` functions gone | `hiera3_function` |
450
- | PSON removed | Binary serialization changed | (runtime) |
451
- | String literals frozen | Immutable strings | (runtime) |
452
- | Ruby 3.2 required | API changes | (gemspec) |
453
- | `Deferred` lazy evaluation | Resource ordering matters | (runtime) |
454
- | `Sensitive` auto-protection | Deferred functions protected | (runtime) |
676
+ Boolean values `true` and `false` should not be quoted.
455
677
 
456
- ### OpenVox Compatibility
678
+ **Why:** Quoted booleans are strings, not booleans. `'true'` is truthy
679
+ because it's a non-empty string, but it won't work correctly with strict
680
+ boolean comparisons or type checking.
457
681
 
458
- OpenVox 8.x is a **fully compatible fork** of Puppet 8.x:
459
- - **Identical language syntax** — no changes to the Puppet DSL
460
- - **Identical module compatibility** — all Puppet Forge modules work
461
- - **Different package names** — `openvox-agent` replaces `puppet-agent`
462
- - **Same configuration paths** — `/etc/puppetlabs/`
463
- - **Community maintained** by Vox Pupuli
682
+ **Bad:**
683
+ ```puppet
684
+ $enabled = 'true'
685
+ service { 'nginx': enable => "false" }
686
+ ```
464
687
 
465
- openvox-lint works identically with both OpenVox and Puppet manifests.
688
+ **Good:**
689
+ ```puppet
690
+ $enabled = true
691
+ service { 'nginx': enable => false }
692
+ ```
466
693
 
467
694
  ---
468
695
 
469
- ## Exit Codes
696
+ ### Variables (2 checks)
470
697
 
471
- | Code | Meaning |
472
- |------|---------|
473
- | `0` | No errors (warnings allowed unless `--fail-on-warnings`) |
474
- | `1` | Errors found, or warnings with `--fail-on-warnings` |
698
+ #### `variable_is_lowercase` (WARNING)
699
+
700
+ Variable names must be lowercase. Names may contain underscores and colons
701
+ (for namespaced variables).
702
+
703
+ **Why:** Puppet convention is lowercase variables. Mixed case can cause
704
+ confusion with class references which are capitalised.
705
+
706
+ **Bad:**
707
+ ```puppet
708
+ $MyVariable = 'value'
709
+ $DatabasePort = 5432
710
+ ```
711
+
712
+ **Good:**
713
+ ```puppet
714
+ $my_variable = 'value'
715
+ $database_port = 5432
716
+ ```
475
717
 
476
718
  ---
477
719
 
478
- ## Plugin Development
720
+ #### `variable_contains_dash` (WARNING)
479
721
 
480
- ### Creating a Check Plugin
722
+ Variable names must not contain dashes (hyphens).
481
723
 
482
- ```ruby
483
- # my_check.rb
484
- OpenvoxLint.new_check(:my_check) do
485
- def check
486
- tokens.each do |tok|
487
- if tok.type == :NAME && tok.value == 'bad_thing'
488
- notify :warning,
489
- message: 'found bad_thing',
490
- line: tok.line,
491
- column: tok.column
492
- end
493
- end
494
- end
724
+ **Why:** Dashes are not valid in Puppet variable names. The parser may
725
+ interpret them as subtraction operations.
495
726
 
496
- # Optional: auto-fix
497
- def fix(problem)
498
- # Modify tokens in-place
499
- raise OpenvoxLint::NoFix # if can't fix
500
- end
501
- end
727
+ **Bad:**
728
+ ```puppet
729
+ $my-variable = 'value'
502
730
  ```
503
731
 
504
- ### Distributing as a Gem
732
+ **Good:**
733
+ ```puppet
734
+ $my_variable = 'value'
735
+ ```
505
736
 
506
- ```ruby
507
- # my-openvox-lint-check.gemspec
508
- Gem::Specification.new do |s|
509
- s.name = 'openvox-lint-my_check'
510
- s.add_runtime_dependency 'openvox-lint', '~> 1.0'
511
- end
737
+ ---
738
+
739
+ ### Resources (7 checks)
740
+
741
+ #### `ensure_first_param` (WARNING)
742
+
743
+ The `ensure` attribute should be the first parameter in a resource body.
744
+
745
+ **Why:** `ensure` is the most important attribute — it determines whether
746
+ the resource exists. Placing it first makes resource declarations scannable.
747
+
748
+ **Bad:**
749
+ ```puppet
750
+ file { '/tmp/foo':
751
+ owner => 'root',
752
+ ensure => file,
753
+ mode => '0644',
754
+ }
512
755
  ```
513
756
 
514
- Place the check file in `lib/openvox-lint/plugins/checks/my_check.rb`.
757
+ **Good:**
758
+ ```puppet
759
+ file { '/tmp/foo':
760
+ ensure => file,
761
+ owner => 'root',
762
+ mode => '0644',
763
+ }
764
+ ```
515
765
 
516
766
  ---
517
767
 
518
- ## File Inventory
768
+ #### `ensure_not_symlink_target` (WARNING)
769
+
770
+ Symlinks should use `ensure => link` with a `target` attribute, not
771
+ `ensure => '/path/to/target'`.
519
772
 
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 |
773
+ **Why:** Setting `ensure` to a path is confusing. The explicit `link` +
774
+ `target` syntax is clearer and matches documentation examples.
775
+
776
+ **Bad:**
777
+ ```puppet
778
+ file { '/usr/local/bin/python':
779
+ ensure => '/usr/bin/python3',
780
+ }
781
+ ```
782
+
783
+ **Good:**
784
+ ```puppet
785
+ file { '/usr/local/bin/python':
786
+ ensure => link,
787
+ target => '/usr/bin/python3',
788
+ }
789
+ ```
790
+
791
+ ---
792
+
793
+ #### `file_mode` (WARNING)
794
+
795
+ File modes should be 4-digit quoted octal strings or symbolic modes.
796
+
797
+ **Why:** 3-digit modes are ambiguous (is `644` the same as `0644`?).
798
+ Unquoted numbers can be misinterpreted. Symbolic modes (`u+x`) are allowed.
799
+
800
+ **Bad:**
801
+ ```puppet
802
+ file { '/tmp/foo':
803
+ mode => 644, # Unquoted, 3 digits
804
+ }
805
+ file { '/tmp/bar':
806
+ mode => '755', # Only 3 digits
807
+ }
808
+ ```
809
+
810
+ **Good:**
811
+ ```puppet
812
+ file { '/tmp/foo':
813
+ mode => '0644', # 4-digit quoted octal
814
+ }
815
+ file { '/tmp/bar':
816
+ mode => 'u+x', # Symbolic mode
817
+ }
818
+ ```
819
+
820
+ ---
821
+
822
+ #### `unquoted_file_mode` (WARNING)
823
+
824
+ File modes must be quoted strings, not bare numbers.
825
+
826
+ **Why:** Numeric file modes are parsed as integers. `0644` is valid octal,
827
+ but `644` might be decimal. Quoting removes ambiguity.
828
+
829
+ **Bad:**
830
+ ```puppet
831
+ file { '/tmp/foo':
832
+ mode => 0644,
833
+ }
834
+ ```
835
+
836
+ **Good:**
837
+ ```puppet
838
+ file { '/tmp/foo':
839
+ mode => '0644',
840
+ }
841
+ ```
842
+
843
+ ---
844
+
845
+ #### `unquoted_resource_title` (WARNING)
846
+
847
+ Resource titles should be quoted strings, not bare words.
848
+
849
+ **Why:** Bare word titles can be confused with variables or cause unexpected
850
+ behaviour. Quoting makes intent explicit.
851
+
852
+ **Bad:**
853
+ ```puppet
854
+ file { tmpfile:
855
+ ensure => file,
856
+ }
857
+ ```
858
+
859
+ **Good:**
860
+ ```puppet
861
+ file { '/tmp/file':
862
+ ensure => file,
863
+ }
864
+ file { 'configuration file':
865
+ path => '/etc/app.conf',
866
+ ensure => file,
867
+ }
868
+ ```
869
+
870
+ ---
871
+
872
+ #### `duplicate_params` (ERROR)
873
+
874
+ No duplicate parameters in resource declarations.
875
+
876
+ **Why:** Duplicate parameters are almost always mistakes. The last value
877
+ wins, which can cause subtle bugs.
878
+
879
+ **Bad:**
880
+ ```puppet
881
+ file { '/tmp/foo':
882
+ ensure => file,
883
+ owner => 'root',
884
+ owner => 'nobody', # Duplicate!
885
+ }
886
+ ```
887
+
888
+ **Good:**
889
+ ```puppet
890
+ file { '/tmp/foo':
891
+ ensure => file,
892
+ owner => 'root',
893
+ }
894
+ ```
895
+
896
+ ---
897
+
898
+ #### `trailing_comma` (WARNING)
899
+
900
+ Resource bodies should end with a trailing comma after the last attribute.
901
+
902
+ **Why:** Trailing commas make diffs cleaner when adding new attributes.
903
+ They also prevent syntax errors when copy-pasting.
904
+
905
+ **Note:** Only fires inside resource bodies (`name { ... }`), not inside
906
+ conditionals, class bodies, or other brace contexts.
907
+
908
+ **Bad:**
909
+ ```puppet
910
+ file { '/tmp/foo':
911
+ ensure => file,
912
+ owner => 'root'
913
+ }
914
+ ```
915
+
916
+ **Good:**
917
+ ```puppet
918
+ file { '/tmp/foo':
919
+ ensure => file,
920
+ owner => 'root',
921
+ }
922
+ ```
923
+
924
+ ---
925
+
926
+ ### Classes & Defines (5 checks)
927
+
928
+ #### `documentation` (WARNING)
929
+
930
+ Classes and defined types should be preceded by documentation comments.
931
+
932
+ **Why:** Documentation helps users understand what a class does without
933
+ reading the implementation. Puppet Strings extracts these comments.
934
+
935
+ **Bad:**
936
+ ```puppet
937
+ class mymodule::webserver {
938
+ # ...
939
+ }
940
+ ```
941
+
942
+ **Good:**
943
+ ```puppet
944
+ # Configures an Nginx webserver with standard settings.
945
+ #
946
+ # @param port The port to listen on.
947
+ # @param ssl Enable SSL termination.
948
+ class mymodule::webserver (
949
+ Integer $port = 80,
950
+ Boolean $ssl = false,
951
+ ) {
952
+ # ...
953
+ }
954
+ ```
955
+
956
+ ---
957
+
958
+ #### `nested_classes_or_defines` (WARNING)
959
+
960
+ Classes and defined types should not be nested inside other classes or
961
+ defined types.
962
+
963
+ **Why:** Nested definitions are confusing and don't work as expected.
964
+ Use separate files and include/contain for composition.
965
+
966
+ **Bad:**
967
+ ```puppet
968
+ class outer {
969
+ class inner { # Nested!
970
+ # ...
971
+ }
972
+ }
973
+ ```
974
+
975
+ **Good:**
976
+ ```puppet
977
+ # outer.pp
978
+ class outer {
979
+ contain outer::inner
980
+ }
981
+
982
+ # inner.pp
983
+ class outer::inner {
984
+ # ...
985
+ }
986
+ ```
987
+
988
+ ---
989
+
990
+ #### `parameter_order` (WARNING)
991
+
992
+ Parameters without defaults should come before parameters with defaults.
993
+
994
+ **Why:** When calling a class, required parameters must be specified.
995
+ Listing them first makes the required inputs obvious.
996
+
997
+ **Bad:**
998
+ ```puppet
999
+ class myclass (
1000
+ $optional = 'default',
1001
+ $required, # No default, but after one with default!
1002
+ ) {
1003
+ }
1004
+ ```
1005
+
1006
+ **Good:**
1007
+ ```puppet
1008
+ class myclass (
1009
+ $required,
1010
+ $optional = 'default',
1011
+ ) {
1012
+ }
1013
+ ```
1014
+
1015
+ ---
1016
+
1017
+ #### `class_inherits_params` (WARNING)
1018
+
1019
+ Class inheritance is discouraged. Use composition (include, contain, require)
1020
+ instead.
1021
+
1022
+ **Why:** Class inheritance was an early Puppet feature that causes confusion.
1023
+ Composition is more flexible and easier to understand.
1024
+
1025
+ **Bad:**
1026
+ ```puppet
1027
+ class mymodule::child inherits mymodule::parent {
1028
+ # ...
1029
+ }
1030
+ ```
1031
+
1032
+ **Good:**
1033
+ ```puppet
1034
+ class mymodule::child {
1035
+ contain mymodule::parent
1036
+ # ...
1037
+ }
1038
+ ```
1039
+
1040
+ ---
1041
+
1042
+ #### `inherits_across_namespaces` (WARNING)
1043
+
1044
+ Classes should not inherit across module namespaces.
1045
+
1046
+ **Why:** Cross-namespace inheritance creates tight coupling between modules.
1047
+ It makes modules harder to maintain and test independently.
1048
+
1049
+ **Bad:**
1050
+ ```puppet
1051
+ class mymodule::foo inherits othermodule::bar {
1052
+ # Inheriting from a different module!
1053
+ }
1054
+ ```
1055
+
1056
+ **Good:**
1057
+ ```puppet
1058
+ class mymodule::foo {
1059
+ contain othermodule::bar # Composition instead
1060
+ }
1061
+ ```
1062
+
1063
+ ---
1064
+
1065
+ ### Conditionals (2 checks)
1066
+
1067
+ #### `case_without_default` (WARNING)
1068
+
1069
+ Case statements must have a `default` case.
1070
+
1071
+ **Why:** Without a default, unexpected values silently do nothing.
1072
+ Explicit defaults catch mistakes and document expected behaviour.
1073
+
1074
+ **Bad:**
1075
+ ```puppet
1076
+ case $os {
1077
+ 'RedHat': { include redhat }
1078
+ 'Debian': { include debian }
1079
+ }
1080
+ ```
1081
+
1082
+ **Good:**
1083
+ ```puppet
1084
+ case $os {
1085
+ 'RedHat': { include redhat }
1086
+ 'Debian': { include debian }
1087
+ default: { fail("Unsupported OS: ${os}") }
1088
+ }
1089
+ ```
1090
+
1091
+ ---
1092
+
1093
+ #### `selector_inside_resource` (WARNING)
1094
+
1095
+ Selectors (`?`) should not be used inside resource declarations.
1096
+
1097
+ **Why:** Selectors inside resource bodies reduce readability. Extract
1098
+ the logic to a variable or use a conditional outside the resource.
1099
+
1100
+ **Bad:**
1101
+ ```puppet
1102
+ file { '/etc/app.conf':
1103
+ content => $env ? {
1104
+ 'prod' => template('app/prod.erb'),
1105
+ default => template('app/dev.erb'),
1106
+ },
1107
+ }
1108
+ ```
1109
+
1110
+ **Good:**
1111
+ ```puppet
1112
+ $config_template = $env ? {
1113
+ 'prod' => 'app/prod.erb',
1114
+ default => 'app/dev.erb',
1115
+ }
1116
+
1117
+ file { '/etc/app.conf':
1118
+ content => template($config_template),
1119
+ }
1120
+ ```
1121
+
1122
+ ---
1123
+
1124
+ ### References & Syntax (3 checks)
1125
+
1126
+ #### `leading_zero` (WARNING)
1127
+
1128
+ Numbers should not have leading zeros (except octal file modes).
1129
+
1130
+ **Why:** Leading zeros indicate octal notation. `010` is 8, not 10.
1131
+ This is a common source of bugs.
1132
+
1133
+ **Exception:** The check allows leading zeros after `mode =>` for file modes.
1134
+
1135
+ **Bad:**
1136
+ ```puppet
1137
+ $count = 010 # This is 8 in octal!
1138
+ $port = 0080 # This is 64!
1139
+ ```
1140
+
1141
+ **Good:**
1142
+ ```puppet
1143
+ $count = 10
1144
+ $port = 80
1145
+ file { '/tmp/foo': mode => '0644' } # Allowed for mode
1146
+ ```
1147
+
1148
+ ---
1149
+
1150
+ #### `resource_reference_without_title_capital` (WARNING)
1151
+
1152
+ Resource reference types must start with a capital letter.
1153
+
1154
+ **Why:** Resource references use capitalised type names: `File['/tmp']`,
1155
+ not `file['/tmp']`. Lowercase looks like an array access.
1156
+
1157
+ **Note:** The check has an allowlist of 40+ functions that use bracket
1158
+ syntax (`each`, `map`, `filter`, `lookup`, etc.) to avoid false positives.
1159
+
1160
+ **Bad:**
1161
+ ```puppet
1162
+ require file['/etc/config']
1163
+ ```
1164
+
1165
+ **Good:**
1166
+ ```puppet
1167
+ require File['/etc/config']
1168
+ ```
1169
+
1170
+ ---
1171
+
1172
+ #### `autoloader_layout` (WARNING)
1173
+
1174
+ Class and define names should match the autoloader file path.
1175
+
1176
+ **Why:** Puppet's autoloader expects `class foo::bar::baz` in
1177
+ `foo/manifests/bar/baz.pp`. Mismatches cause "class not found" errors.
1178
+
1179
+ **Bad:**
1180
+ ```puppet
1181
+ # In mymodule/manifests/init.pp
1182
+ class mymodule::subclass { # Should be in subclass.pp
1183
+ }
1184
+ ```
1185
+
1186
+ **Good:**
1187
+ ```puppet
1188
+ # In mymodule/manifests/init.pp
1189
+ class mymodule {
1190
+ }
1191
+
1192
+ # In mymodule/manifests/subclass.pp
1193
+ class mymodule::subclass {
1194
+ }
1195
+ ```
1196
+
1197
+ ---
1198
+
1199
+ ### Comments (1 check)
1200
+
1201
+ #### `star_comments` (WARNING)
1202
+
1203
+ Use `#` hash comments, not `/* */` block comments.
1204
+
1205
+ **Why:** Hash comments are the standard in Puppet. Block comments are
1206
+ inherited from C-style languages and less common in the ecosystem.
1207
+
1208
+ **Bad:**
1209
+ ```puppet
1210
+ /* This is a block comment
1211
+ that spans multiple lines */
1212
+ class foo {
1213
+ }
1214
+ ```
1215
+
1216
+ **Good:**
1217
+ ```puppet
1218
+ # This is a hash comment
1219
+ # that spans multiple lines
1220
+ class foo {
1221
+ }
1222
+ ```
1223
+
1224
+ ---
1225
+
1226
+ ### URLs (1 check)
1227
+
1228
+ #### `puppet_url_without_modules` (WARNING)
1229
+
1230
+ `puppet:///` URLs should include the `/modules/` mount point.
1231
+
1232
+ **Why:** The full URL format is `puppet:///modules/modulename/path`.
1233
+ Omitting `/modules/` causes file not found errors.
1234
+
1235
+ **Bad:**
1236
+ ```puppet
1237
+ file { '/etc/config':
1238
+ source => 'puppet:///mymodule/config',
1239
+ }
1240
+ ```
1241
+
1242
+ **Good:**
1243
+ ```puppet
1244
+ file { '/etc/config':
1245
+ source => 'puppet:///modules/mymodule/config',
1246
+ }
1247
+ ```
1248
+
1249
+ ---
1250
+
1251
+ ### Nodes (1 check)
1252
+
1253
+ #### `node_name_unquoted` (WARNING)
1254
+
1255
+ Node names should be quoted strings, not bare words.
1256
+
1257
+ **Why:** Quoted names make intent explicit and avoid potential parsing
1258
+ issues with special characters.
1259
+
1260
+ **Bad:**
1261
+ ```puppet
1262
+ node webserver01 {
1263
+ }
1264
+ ```
1265
+
1266
+ **Good:**
1267
+ ```puppet
1268
+ node 'webserver01' {
1269
+ }
1270
+
1271
+ node 'webserver01.example.com' {
1272
+ }
1273
+ ```
1274
+
1275
+ ---
1276
+
1277
+ ### Puppet 8 / OpenVox 8 Compatibility (4 checks)
1278
+
1279
+ These checks are critical for users upgrading from Puppet 7 or migrating
1280
+ to OpenVox 8. They detect patterns that will break or behave differently
1281
+ in the new version.
1282
+
1283
+ #### `legacy_facts` (WARNING)
1284
+
1285
+ Legacy (unstructured) top-scope facts are excluded by default in Puppet 8 /
1286
+ OpenVox 8. Over 80 legacy fact names are detected.
1287
+
1288
+ **Why:** Puppet 8 excludes legacy facts by default. Code using `$osfamily`
1289
+ will break unless the agent is configured to include legacy facts (not
1290
+ recommended for new code).
1291
+
1292
+ **Bad:**
1293
+ ```puppet
1294
+ if $osfamily == 'RedHat' { }
1295
+ $host = $fqdn
1296
+ $ip = $ipaddress
1297
+ $os = $operatingsystem
1298
+ ```
1299
+
1300
+ **Good:**
1301
+ ```puppet
1302
+ if $facts['os']['family'] == 'RedHat' { }
1303
+ $host = $facts['networking']['fqdn']
1304
+ $ip = $facts['networking']['ip']
1305
+ $os = $facts['os']['name']
1306
+ ```
1307
+
1308
+ **Detected legacy facts include:** `architecture`, `bios_*`, `domain`,
1309
+ `fqdn`, `hostname`, `id`, `interfaces`, `ipaddress`, `ipaddress6`,
1310
+ `kernel`, `kernelrelease`, `macaddress`, `memoryfree`, `memorysize`,
1311
+ `netmask`, `network`, `operatingsystem`, `operatingsystemrelease`,
1312
+ `osfamily`, `processorcount`, `puppetversion`, `rubyversion`,
1313
+ `selinux*`, `swapfree`, `swapsize`, `timezone`, `uptime*`, `virtual`,
1314
+ and many more.
1315
+
1316
+ ---
1317
+
1318
+ #### `top_scope_facts` (WARNING)
1319
+
1320
+ Top-scope fact variables (`$::factname`) are deprecated. Use the `$facts`
1321
+ hash instead.
1322
+
1323
+ **Why:** The `$::` prefix was needed in older Puppet to explicitly reference
1324
+ top-scope variables. Modern Puppet provides the `$facts` hash which is
1325
+ clearer and more consistent.
1326
+
1327
+ **Note:** This check only flags simple facts, not module-qualified variables
1328
+ like `$::mymodule::param` which are legitimate.
1329
+
1330
+ **Bad:**
1331
+ ```puppet
1332
+ $hostname = $::hostname
1333
+ $os = $::operatingsystem
1334
+ if $::selinux { }
1335
+ ```
1336
+
1337
+ **Good:**
1338
+ ```puppet
1339
+ $hostname = $facts['networking']['hostname']
1340
+ $os = $facts['os']['name']
1341
+ if $facts['selinux'] { }
1342
+ ```
1343
+
1344
+ ---
1345
+
1346
+ #### `hiera3_function` (ERROR)
1347
+
1348
+ Deprecated Hiera 3 functions (`hiera`, `hiera_array`, `hiera_hash`, `hiera_include`)
1349
+ are **removed** in Puppet 8 / OpenVox 8. Use the Hiera 5 `lookup()` function instead.
1350
+
1351
+ **Why:** Hiera 3 is fully deprecated. Only **Hiera 5** is supported in Puppet 8 /
1352
+ OpenVox 8. The legacy `hiera()` functions were compatibility shims that have been
1353
+ removed. This is an error, not a warning, because the code will fail immediately.
1354
+
1355
+ **Bad — deprecated Hiera 3 functions:**
1356
+ ```puppet
1357
+ $val = hiera('mykey')
1358
+ $list = hiera_array('mylist')
1359
+ $hash = hiera_hash('myhash')
1360
+ hiera_include('classes')
1361
+ ```
1362
+
1363
+ **Good — Hiera 5 lookup() function:**
1364
+ ```puppet
1365
+ $val = lookup('mykey')
1366
+ $list = lookup('mylist', Array, 'unique')
1367
+ $hash = lookup('myhash', Hash, 'hash')
1368
+ lookup('classes', Array[String], 'unique').include
1369
+ ```
1370
+
1371
+ **Hiera 5 Features:**
1372
+ - Single `lookup()` function replaces all Hiera 3 functions
1373
+ - Supports type validation: `lookup('key', String)`
1374
+ - Supports merge strategies: `'first'`, `'unique'`, `'hash'`, `'deep'`
1375
+ - Supports default values: `lookup('key', String, 'first', 'default_value')`
1376
+ - Works with module-level `hiera.yaml` configuration
1377
+
1378
+ ---
1379
+
1380
+ #### `import_statement` (ERROR)
1381
+
1382
+ The `import` keyword was removed in Puppet 4.
1383
+
1384
+ **Why:** This is an error because the code will fail to parse. Use
1385
+ module autoloading instead.
1386
+
1387
+ **Bad:**
1388
+ ```puppet
1389
+ import 'foo'
1390
+ import 'nodes/*.pp'
1391
+ ```
1392
+
1393
+ **Good:**
1394
+
1395
+ Use the module autoloader by placing files in the correct location:
1396
+ - `modules/mymodule/manifests/init.pp` for `class mymodule`
1397
+ - `modules/mymodule/manifests/subclass.pp` for `class mymodule::subclass`
1398
+
1399
+ ---
1400
+
1401
+ ## Token Types Reference
1402
+
1403
+ ### Keywords
1404
+
1405
+ | Type | Keyword | Type | Keyword |
1406
+ |------|---------|------|---------|
1407
+ | `:AND` | `and` | `:APPLICATION` | `application` |
1408
+ | `:ATTR` | `attr` | `:CASE` | `case` |
1409
+ | `:CLASS` | `class` | `:CONSUMES` | `consumes` |
1410
+ | `:DEFAULT` | `default` | `:DEFINE` | `define` |
1411
+ | `:ELSE` | `else` | `:ELSIF` | `elsif` |
1412
+ | `:FALSE` | `false` | `:FUNCTION` | `function` |
1413
+ | `:IF` | `if` | `:IMPORT` | `import` |
1414
+ | `:IN` | `in` | `:INHERITS` | `inherits` |
1415
+ | `:NODE` | `node` | `:NOT` | `not` |
1416
+ | `:OR` | `or` | `:PRIVATE` | `private` |
1417
+ | `:PRODUCES` | `produces` | `:SITE` | `site` |
1418
+ | `:TRUE` | `true` | `:TYPE` | `type` |
1419
+ | `:UNDEF` | `undef` | `:UNLESS` | `unless` |
1420
+
1421
+ ### Identifiers & Literals
1422
+
1423
+ | Type | Description | Example |
1424
+ |------|-------------|---------|
1425
+ | `:NAME` | Identifier / bare word | `ensure`, `myclass` |
1426
+ | `:CLASSREF` | Capitalised type reference | `File`, `String`, `Stdlib::Absolutepath` |
1427
+ | `:VARIABLE` | Variable (includes `$`) | `$foo`, `$::bar::baz` |
1428
+ | `:NUMBER` | Numeric literal | `42`, `0xFF`, `0755`, `3.14`, `1e10` |
1429
+ | `:SSTRING` | Single-quoted string | `'hello'` |
1430
+ | `:STRING` | Double-quoted string (no interpolation) | `"hello"` |
1431
+ | `:DQSTRING` | Double-quoted string (with interpolation) | `"hello ${name}"` |
1432
+ | `:REGEX` | Regular expression | `/^foo/` |
1433
+ | `:HEREDOC_OPEN` | Heredoc opening tag | `@("END")` |
1434
+ | `:HEREDOC` | Heredoc body content | (multi-line content) |
1435
+
1436
+ ### Operators
1437
+
1438
+ | Type | Operator | Description |
1439
+ |------|----------|-------------|
1440
+ | `:FARROW` | `=>` | Hash rocket (parameter assignment) |
1441
+ | `:PARROW` | `+>` | Append to array attribute |
1442
+ | `:ISEQUAL` | `==` | Equality |
1443
+ | `:NOTEQUAL` | `!=` | Inequality |
1444
+ | `:MATCH` | `=~` | Regex match |
1445
+ | `:NOMATCH` | `!~` | Regex non-match |
1446
+ | `:LESSEQUAL` | `<=` | Less than or equal |
1447
+ | `:GREATEREQUAL` | `>=` | Greater than or equal |
1448
+ | `:LESSTHAN` | `<` | Less than |
1449
+ | `:GREATERTHAN` | `>` | Greater than |
1450
+ | `:LSHIFT` | `<<` | Left shift |
1451
+ | `:RSHIFT` | `>>` | Right shift |
1452
+ | `:IN_EDGE` | `->` | Ordering (before) |
1453
+ | `:OUT_EDGE` | `<-` | Ordering (after) |
1454
+ | `:IN_EDGE_SUB` | `~>` | Ordering with notify |
1455
+ | `:OUT_EDGE_SUB` | `<~` | Reverse ordering with notify |
1456
+ | `:APPENDS` | `+=` | Append assignment |
1457
+ | `:EQUALS` | `=` | Assignment |
1458
+ | `:LCOLLECT` | `<\|` | Collection query open |
1459
+ | `:RCOLLECT` | `\|>` | Collection query close |
1460
+ | `:LLCOLLECT` | `<<\|` | Exported collection query open |
1461
+ | `:RRCOLLECT` | `\|>>` | Exported collection query close |
1462
+
1463
+ ### Punctuation
1464
+
1465
+ | Type | Character | Type | Character |
1466
+ |------|-----------|------|-----------|
1467
+ | `:LBRACE` | `{` | `:RBRACE` | `}` |
1468
+ | `:LPAREN` | `(` | `:RPAREN` | `)` |
1469
+ | `:LBRACK` | `[` | `:RBRACK` | `]` |
1470
+ | `:COMMA` | `,` | `:SEMIC` | `;` |
1471
+ | `:DOT` | `.` | `:COLON` | `:` |
1472
+ | `:PIPE` | `\|` | `:AT` | `@` |
1473
+ | `:QMARK` | `?` | `:BACKSLASH` | `\\` |
1474
+ | `:PLUS` | `+` | `:MINUS` | `-` |
1475
+ | `:TIMES` | `*` | `:MODULO` | `%` |
1476
+ | `:DIV` | `/` | `:NOT` | `!` |
1477
+
1478
+ ### Formatting Tokens
1479
+
1480
+ | Type | Description |
1481
+ |------|-------------|
1482
+ | `:WHITESPACE` | Spaces/tabs (not at line start) |
1483
+ | `:INDENT` | Spaces/tabs at line start |
1484
+ | `:NEWLINE` | Line break (`\n` or `\r\n`) |
1485
+ | `:COMMENT` | Hash comment (`# ...`) |
1486
+ | `:MLCOMMENT` | Multi-line comment (`/* ... */`) |
1487
+ | `:SLASH_COMMENT` | C++ style comment (`// ...`) |
1488
+
1489
+ ---
1490
+
1491
+ ## Plugin Development
1492
+
1493
+ ### Creating a Check Plugin
1494
+
1495
+ ```ruby
1496
+ # lib/openvox-lint/plugins/checks/my_custom_check.rb
1497
+ OpenvoxLint.new_check(:my_custom_check) do
1498
+ def check
1499
+ tokens.each do |tok|
1500
+ if tok.type == :NAME && tok.value == 'deprecated_function'
1501
+ notify :warning,
1502
+ message: 'deprecated_function() is obsolete',
1503
+ line: tok.line,
1504
+ column: tok.column
1505
+ end
1506
+ end
1507
+ end
1508
+
1509
+ # Optional: implement auto-fix
1510
+ def fix(problem)
1511
+ # Find and modify the problematic token
1512
+ # Or raise NoFix if this instance can't be fixed
1513
+ raise OpenvoxLint::NoFix
1514
+ end
1515
+ end
1516
+ ```
1517
+
1518
+ ### Using Helper Methods
1519
+
1520
+ ```ruby
1521
+ OpenvoxLint.new_check(:resource_check) do
1522
+ def check
1523
+ # Check only resource bodies
1524
+ resource_indexes.each do |resource|
1525
+ type_name = resource[:type].value
1526
+ next unless type_name == 'file'
1527
+
1528
+ # Examine parameters
1529
+ resource[:param_tokens].each do |tok|
1530
+ # Check parameter values
1531
+ end
1532
+ end
1533
+ end
1534
+ end
1535
+ ```
1536
+
1537
+ ### Line-Based Checks
1538
+
1539
+ ```ruby
1540
+ OpenvoxLint.new_check(:line_based_check) do
1541
+ def check
1542
+ manifest_lines.each_with_index do |line, idx|
1543
+ if line =~ /FIXME|TODO/
1544
+ notify :warning,
1545
+ message: 'TODO/FIXME comment found',
1546
+ line: idx + 1, # Lines are 1-indexed in output
1547
+ column: 1
1548
+ end
1549
+ end
1550
+ end
1551
+ end
1552
+ ```
1553
+
1554
+ ### Distributing as a Gem
1555
+
1556
+ ```ruby
1557
+ # openvox-lint-my_checks.gemspec
1558
+ Gem::Specification.new do |spec|
1559
+ spec.name = 'openvox-lint-my_checks'
1560
+ spec.version = '1.0.0'
1561
+ spec.summary = 'Custom checks for openvox-lint'
1562
+
1563
+ spec.add_runtime_dependency 'openvox-lint', '~> 1.0'
1564
+
1565
+ spec.files = Dir['lib/**/*']
1566
+ spec.require_paths = ['lib']
1567
+ end
1568
+ ```
1569
+
1570
+ Place check files in `lib/openvox-lint/plugins/checks/` and they will
1571
+ be auto-loaded when the gem is required.
1572
+
1573
+ ---
1574
+
1575
+ ## Migration from puppet-lint
1576
+
1577
+ openvox-lint is designed as a modern replacement for puppet-lint with
1578
+ broader compatibility and additional checks.
1579
+
1580
+ ### Key Differences
1581
+
1582
+ | Feature | puppet-lint 5.x | openvox-lint 1.x |
1583
+ |---------|-----------------|------------------|
1584
+ | Ruby requirement | ≥ 3.1 | ≥ 2.5 (works on RHEL 8, macOS system Ruby) |
1585
+ | Runtime dependencies | None | None |
1586
+ | Built-in checks | ~25 | 37 |
1587
+ | Legacy facts detection | Via plugin | Built-in |
1588
+ | Top-scope facts detection | Via plugin | Built-in |
1589
+ | Deprecated Hiera 3 function detection | No | Built-in (ERROR) |
1590
+ | Import statement detection | No | Built-in (ERROR) |
1591
+ | Strict indent check | Via plugin | Built-in |
1592
+ | GitHub Actions output | No | Built-in (`-f github`) |
1593
+ | Code Climate output | No | Built-in (`-f codeclimate`) |
1594
+ | CSV output | No | Built-in (`-f csv`) |
1595
+ | OpenVox awareness | No | Yes |
1596
+ | `--fix` support | Yes | Yes |
1597
+ | Plugin system | Yes | Yes (compatible API) |
1598
+
1599
+ ### Command-Line Compatibility
1600
+
1601
+ Most puppet-lint flags work identically:
1602
+
1603
+ ```bash
1604
+ # These work the same way
1605
+ puppet-lint --no-documentation-check manifests/
1606
+ openvox-lint --no-documentation-check manifests/
1607
+
1608
+ puppet-lint --only-checks legacy_facts,hiera3_function .
1609
+ openvox-lint --only-checks legacy_facts,hiera3_function .
1610
+
1611
+ puppet-lint --log-format '%{path}:%{line}:%{KIND}' .
1612
+ openvox-lint --log-format '%{path}:%{line}:%{KIND}' .
1613
+ ```
1614
+
1615
+ ### RC File Migration
1616
+
1617
+ Rename `.puppet-lint.rc` to `.openvox-lint.rc`. The format is identical:
1618
+
1619
+ ```bash
1620
+ # .openvox-lint.rc
1621
+ --no-documentation-check
1622
+ --no-line_length-check
1623
+ --fail-on-warnings
1624
+ ```
1625
+
1626
+ ### Plugin Migration
1627
+
1628
+ Plugin APIs are similar. Main differences:
1629
+
1630
+ 1. Module name: `OpenvoxLint` instead of `PuppetLint`
1631
+ 2. Check registration: `OpenvoxLint.new_check(:name)` instead of `PuppetLint.new_check(:name)`
1632
+
1633
+ ---
1634
+
1635
+ ## Exit Codes
1636
+
1637
+ | Code | Meaning |
1638
+ |------|---------|
1639
+ | `0` | No errors found (warnings allowed unless `--fail-on-warnings`) |
1640
+ | `1` | Errors found, or warnings found with `--fail-on-warnings` |
1641
+
1642
+ ---
1643
+
1644
+ ## File Inventory
1645
+
1646
+ | File | Description |
1647
+ |------|-------------|
1648
+ | `bin/openvox-lint` | CLI entry point (Ruby executable) |
1649
+ | `lib/openvox-lint.rb` | Main module, auto-loads all components |
1650
+ | `lib/openvox-lint/version.rb` | Version constant |
1651
+ | `lib/openvox-lint/configuration.rb` | Configuration management |
1652
+ | `lib/openvox-lint/token.rb` | Token data structure |
1653
+ | `lib/openvox-lint/lexer.rb` | Puppet/OpenVox manifest lexer |
1654
+ | `lib/openvox-lint/check_plugin.rb` | Base class for check plugins |
1655
+ | `lib/openvox-lint/checks.rb` | Check runner with lint:ignore support |
1656
+ | `lib/openvox-lint/report.rb` | Output formatters (text, json, etc.) |
1657
+ | `lib/openvox-lint/linter.rb` | File discovery and orchestration |
1658
+ | `lib/openvox-lint/cli.rb` | Command-line interface |
1659
+ | `lib/openvox-lint/plugins/checks/*.rb` | 37 built-in check plugins |
1660
+ | `spec/spec_helper.rb` | RSpec test helper |
1661
+ | `spec/unit/lexer_spec.rb` | Lexer unit tests |
1662
+ | `spec/unit/checks_spec.rb` | Check unit tests |
1663
+ | `openvox-lint.gemspec` | Gem specification |
1664
+ | `Gemfile` | Development dependencies |
1665
+ | `Rakefile` | Rake tasks |
1666
+ | `LICENSE` | Apache 2.0 license |
1667
+ | `README.md` | User documentation |
1668
+ | `CHANGELOG.md` | Version history |
1669
+ | `DOCUMENTATION.md` | This file |