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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +178 -1
- data/DOCUMENTATION.md +1392 -259
- data/README.md +53 -15
- data/lib/openvox-lint/check_plugin.rb +2 -1
- data/lib/openvox-lint/checks.rb +47 -3
- data/lib/openvox-lint/cli.rb +13 -4
- data/lib/openvox-lint/configuration.rb +2 -3
- data/lib/openvox-lint/lexer.rb +23 -9
- data/lib/openvox-lint/linter.rb +25 -1
- data/lib/openvox-lint/plugins/checks/arrow_alignment.rb +38 -0
- data/lib/openvox-lint/plugins/checks/double_quoted_strings.rb +16 -0
- data/lib/openvox-lint/plugins/checks/duplicate_params.rb +2 -2
- data/lib/openvox-lint/plugins/checks/hard_tabs.rb +7 -0
- data/lib/openvox-lint/plugins/checks/hiera3_function.rb +13 -3
- data/lib/openvox-lint/plugins/checks/legacy_facts.rb +47 -1
- data/lib/openvox-lint/plugins/checks/parameter_order.rb +2 -2
- data/lib/openvox-lint/plugins/checks/quoted_booleans.rb +21 -0
- data/lib/openvox-lint/plugins/checks/resource_reference_without_title_capital.rb +17 -2
- data/lib/openvox-lint/plugins/checks/single_quote_string_with_variables.rb +16 -0
- data/lib/openvox-lint/plugins/checks/top_scope_facts.rb +3 -1
- data/lib/openvox-lint/plugins/checks/trailing_comma.rb +20 -14
- data/lib/openvox-lint/plugins/checks/trailing_whitespace.rb +6 -0
- data/lib/openvox-lint/plugins/checks/variables_not_enclosed.rb +13 -7
- data/lib/openvox-lint/version.rb +1 -1
- data/lib/openvox-lint.rb +10 -0
- metadata +16 -12
- data/lib/openvox-lint/plugins/checks/relative_classname_inclusion.rb +0 -24
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
|
-
##
|
|
87
|
+
## API Reference
|
|
88
|
+
|
|
89
|
+
### Module: OpenvoxLint
|
|
90
|
+
|
|
91
|
+
The top-level namespace for all openvox-lint classes.
|
|
44
92
|
|
|
45
|
-
|
|
93
|
+
#### Constants
|
|
46
94
|
|
|
47
95
|
| Constant | Value | Description |
|
|
48
96
|
|----------|-------|-------------|
|
|
49
|
-
| `VERSION` | `'1.0
|
|
97
|
+
| `VERSION` | `'1.3.0'` | Gem version string |
|
|
50
98
|
|
|
51
|
-
|
|
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
|
-
|
|
109
|
+
#### Exceptions
|
|
61
110
|
|
|
62
111
|
| Exception | Inherits | Usage |
|
|
63
112
|
|-----------|----------|-------|
|
|
64
|
-
| `OpenvoxLint::Error` | `StandardError` | General errors |
|
|
65
|
-
| `OpenvoxLint::NoFix` | `StandardError` | Raised to skip
|
|
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
|
-
|
|
133
|
+
### Class: OpenvoxLint::Token
|
|
70
134
|
|
|
71
|
-
Represents a single token
|
|
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
|
-
|
|
138
|
+
#### Attributes
|
|
74
139
|
|
|
75
140
|
| Attribute | Type | Description |
|
|
76
141
|
|-----------|------|-------------|
|
|
77
|
-
| `type` | `Symbol` | Token type (see Token Types
|
|
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
|
-
|
|
149
|
+
#### Methods
|
|
85
150
|
|
|
86
151
|
| Method | Returns | Description |
|
|
87
152
|
|--------|---------|-------------|
|
|
88
|
-
| `#formatting?` | `Boolean` | True if whitespace
|
|
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
|
-
|
|
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
|
-
|
|
159
|
+
The following types return `true` for `#formatting?`:
|
|
171
160
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
#### Constructor
|
|
186
176
|
|
|
187
177
|
```ruby
|
|
188
178
|
lexer = OpenvoxLint::Lexer.new(code_string)
|
|
189
179
|
```
|
|
190
180
|
|
|
191
|
-
|
|
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
|
-
|
|
205
|
+
### Class: OpenvoxLint::CheckPlugin
|
|
201
206
|
|
|
202
|
-
Base class for all checks.
|
|
207
|
+
Base class for all lint checks. Create new checks via `OpenvoxLint.new_check`.
|
|
203
208
|
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
+
### Class: OpenvoxLint::Configuration
|
|
239
255
|
|
|
240
|
-
|
|
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
|
-
| `
|
|
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
|
-
|
|
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)` |
|
|
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
|
-
|
|
294
|
+
### Class: OpenvoxLint::Linter
|
|
266
295
|
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
347
|
+
### Class: OpenvoxLint::Report
|
|
291
348
|
|
|
292
|
-
|
|
349
|
+
Formats and outputs lint problems in various formats.
|
|
350
|
+
|
|
351
|
+
#### Constructor
|
|
293
352
|
|
|
294
353
|
```ruby
|
|
295
|
-
report = OpenvoxLint::Report.new(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
320
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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
|
|
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
|
|
343
|
-
|
|
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
|
|
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
|
-
|
|
366
|
-
flagged.
|
|
525
|
+
One parameter means nothing to align with — extra spaces flagged.
|
|
367
526
|
|
|
368
527
|
---
|
|
369
528
|
|
|
370
|
-
###
|
|
529
|
+
### Arrow Alignment (1 check)
|
|
371
530
|
|
|
372
|
-
|
|
373
|
-
migrating to OpenVox.
|
|
531
|
+
#### `arrow_alignment` (WARNING)
|
|
374
532
|
|
|
375
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
580
|
+
file { "/tmp/foo":
|
|
581
|
+
ensure => "present",
|
|
582
|
+
}
|
|
384
583
|
```
|
|
385
584
|
|
|
386
585
|
**Good:**
|
|
387
586
|
```puppet
|
|
388
|
-
|
|
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
|
-
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
#### `only_variable_string` (WARNING)
|
|
392
602
|
|
|
393
|
-
|
|
603
|
+
A string containing only a variable should not be quoted.
|
|
394
604
|
|
|
395
|
-
|
|
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
|
-
$
|
|
610
|
+
$result = "${some_variable}"
|
|
611
|
+
file { "${path}": }
|
|
400
612
|
```
|
|
401
613
|
|
|
402
614
|
**Good:**
|
|
403
615
|
```puppet
|
|
404
|
-
$
|
|
616
|
+
$result = $some_variable
|
|
617
|
+
file { $path: }
|
|
405
618
|
```
|
|
406
619
|
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
414
|
-
|
|
632
|
+
notify { 'hello':
|
|
633
|
+
message => 'Hello $name, welcome!', # $name is literal
|
|
634
|
+
}
|
|
415
635
|
```
|
|
416
636
|
|
|
417
637
|
**Good:**
|
|
418
638
|
```puppet
|
|
419
|
-
|
|
420
|
-
|
|
639
|
+
notify { 'hello':
|
|
640
|
+
message => "Hello ${name}, welcome!", # $name is interpolated
|
|
641
|
+
}
|
|
421
642
|
```
|
|
422
643
|
|
|
423
|
-
|
|
644
|
+
---
|
|
424
645
|
|
|
425
|
-
|
|
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
|
-
|
|
655
|
+
$msg = "Hello $name!"
|
|
656
|
+
$path = "/home/$user/bin"
|
|
430
657
|
```
|
|
431
658
|
|
|
432
659
|
**Good:**
|
|
433
|
-
|
|
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
|
-
|
|
668
|
+
```puppet
|
|
669
|
+
$msg = "Hello $name, your home is ${home}" # Only $name flagged
|
|
670
|
+
```
|
|
438
671
|
|
|
439
|
-
|
|
440
|
-
language changes:
|
|
672
|
+
---
|
|
441
673
|
|
|
442
|
-
|
|
674
|
+
#### `quoted_booleans` (WARNING)
|
|
443
675
|
|
|
444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
- **Community maintained** by Vox Pupuli
|
|
682
|
+
**Bad:**
|
|
683
|
+
```puppet
|
|
684
|
+
$enabled = 'true'
|
|
685
|
+
service { 'nginx': enable => "false" }
|
|
686
|
+
```
|
|
464
687
|
|
|
465
|
-
|
|
688
|
+
**Good:**
|
|
689
|
+
```puppet
|
|
690
|
+
$enabled = true
|
|
691
|
+
service { 'nginx': enable => false }
|
|
692
|
+
```
|
|
466
693
|
|
|
467
694
|
---
|
|
468
695
|
|
|
469
|
-
|
|
696
|
+
### Variables (2 checks)
|
|
470
697
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
720
|
+
#### `variable_contains_dash` (WARNING)
|
|
479
721
|
|
|
480
|
-
|
|
722
|
+
Variable names must not contain dashes (hyphens).
|
|
481
723
|
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
732
|
+
**Good:**
|
|
733
|
+
```puppet
|
|
734
|
+
$my_variable = 'value'
|
|
735
|
+
```
|
|
505
736
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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 |
|