openvox-lint 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +52 -0
  3. data/DOCUMENTATION.md +481 -0
  4. data/LICENSE +83 -0
  5. data/README.md +497 -0
  6. data/bin/openvox-lint +7 -0
  7. data/lib/openvox-lint/check_plugin.rb +147 -0
  8. data/lib/openvox-lint/checks.rb +46 -0
  9. data/lib/openvox-lint/cli.rb +87 -0
  10. data/lib/openvox-lint/configuration.rb +59 -0
  11. data/lib/openvox-lint/lexer.rb +342 -0
  12. data/lib/openvox-lint/linter.rb +72 -0
  13. data/lib/openvox-lint/plugins/checks/arrow_alignment.rb +42 -0
  14. data/lib/openvox-lint/plugins/checks/autoloader_layout.rb +31 -0
  15. data/lib/openvox-lint/plugins/checks/case_without_default.rb +28 -0
  16. data/lib/openvox-lint/plugins/checks/class_inherits_params.rb +13 -0
  17. data/lib/openvox-lint/plugins/checks/documentation.rb +26 -0
  18. data/lib/openvox-lint/plugins/checks/double_quoted_strings.rb +19 -0
  19. data/lib/openvox-lint/plugins/checks/duplicate_params.rb +24 -0
  20. data/lib/openvox-lint/plugins/checks/ensure_first_param.rb +28 -0
  21. data/lib/openvox-lint/plugins/checks/ensure_not_symlink_target.rb +29 -0
  22. data/lib/openvox-lint/plugins/checks/file_mode.rb +33 -0
  23. data/lib/openvox-lint/plugins/checks/hard_tabs.rb +15 -0
  24. data/lib/openvox-lint/plugins/checks/hiera3_function.rb +16 -0
  25. data/lib/openvox-lint/plugins/checks/import_statement.rb +13 -0
  26. data/lib/openvox-lint/plugins/checks/inherits_across_namespaces.rb +27 -0
  27. data/lib/openvox-lint/plugins/checks/leading_zero.rb +22 -0
  28. data/lib/openvox-lint/plugins/checks/legacy_facts.rb +47 -0
  29. data/lib/openvox-lint/plugins/checks/line_length.rb +18 -0
  30. data/lib/openvox-lint/plugins/checks/nested_classes_or_defines.rb +26 -0
  31. data/lib/openvox-lint/plugins/checks/node_name_unquoted.rb +18 -0
  32. data/lib/openvox-lint/plugins/checks/only_variable_string.rb +18 -0
  33. data/lib/openvox-lint/plugins/checks/parameter_order.rb +25 -0
  34. data/lib/openvox-lint/plugins/checks/puppet_url_without_modules.rb +17 -0
  35. data/lib/openvox-lint/plugins/checks/quoted_booleans.rb +16 -0
  36. data/lib/openvox-lint/plugins/checks/relative_classname_inclusion.rb +24 -0
  37. data/lib/openvox-lint/plugins/checks/resource_reference_without_title_capital.rb +21 -0
  38. data/lib/openvox-lint/plugins/checks/selector_inside_resource.rb +15 -0
  39. data/lib/openvox-lint/plugins/checks/single_quote_string_with_variables.rb +16 -0
  40. data/lib/openvox-lint/plugins/checks/space_before_arrow.rb +20 -0
  41. data/lib/openvox-lint/plugins/checks/star_comments.rb +13 -0
  42. data/lib/openvox-lint/plugins/checks/strict_indent.rb +16 -0
  43. data/lib/openvox-lint/plugins/checks/top_scope_facts.rb +19 -0
  44. data/lib/openvox-lint/plugins/checks/trailing_comma.rb +24 -0
  45. data/lib/openvox-lint/plugins/checks/trailing_whitespace.rb +14 -0
  46. data/lib/openvox-lint/plugins/checks/unquoted_file_mode.rb +24 -0
  47. data/lib/openvox-lint/plugins/checks/unquoted_resource_title.rb +13 -0
  48. data/lib/openvox-lint/plugins/checks/variable_contains_dash.rb +15 -0
  49. data/lib/openvox-lint/plugins/checks/variable_is_lowercase.rb +16 -0
  50. data/lib/openvox-lint/plugins/checks/variables_not_enclosed.rb +19 -0
  51. data/lib/openvox-lint/report.rb +86 -0
  52. data/lib/openvox-lint/token.rb +38 -0
  53. data/lib/openvox-lint/version.rb +5 -0
  54. data/lib/openvox-lint.rb +47 -0
  55. metadata +145 -0
data/README.md ADDED
@@ -0,0 +1,497 @@
1
+ # openvox-lint
2
+
3
+ **A style-guide linter for OpenVox and Puppet manifests.**
4
+
5
+ openvox-lint checks your `.pp` manifest files against the [Puppet Style Guide](https://puppet.com/docs/puppet/latest/style_guide.html) and catches common errors, deprecated patterns, legacy facts, strict-mode violations, and Puppet 8+ / OpenVox 8.x language issues.
6
+
7
+ Fully compatible with:
8
+ - **OpenVox 8.x** (the community-maintained open-source fork of Puppet)
9
+ - **Puppet 8.x**
10
+ - Legacy Puppet 7.x manifests (with deprecation warnings)
11
+
12
+ ---
13
+
14
+ ## Table of Contents
15
+
16
+ - [Prerequisites](#prerequisites)
17
+ - [Installation](#installation)
18
+ - [Quick Start](#quick-start)
19
+ - [Usage](#usage)
20
+ - [Built-in Checks (38)](#built-in-checks)
21
+ - [Configuration](#configuration)
22
+ - [Output Formats](#output-formats)
23
+ - [Integration](#integration)
24
+ - [Writing Custom Checks](#writing-custom-checks)
25
+ - [Development](#development)
26
+ - [License](#license)
27
+
28
+ ---
29
+
30
+ ## Prerequisites
31
+
32
+ | Requirement | Version | Notes |
33
+ |------------|---------|-------|
34
+ | **Ruby** | ≥ 3.1.0 | Required; check with `ruby --version` |
35
+ | **RubyGems** | ≥ 3.0 | Included with Ruby 3.1+ |
36
+ | **Bundler** | ≥ 2.0 | `gem install bundler` if not present |
37
+ | **OpenVox or Puppet** | 8.x | Optional; openvox-lint works standalone without an agent |
38
+ | **Git** | ≥ 2.0 | For installation from source |
39
+
40
+ ### Optional Prerequisites
41
+
42
+ | Tool | Purpose |
43
+ |------|---------|
44
+ | `puppet` or `openvox` binary | For `puppet parser validate` syntax checking |
45
+ | `metadata-json-lint` gem | For linting module `metadata.json` files |
46
+ | `vim-openvox` Vim plugin | For IDE integration (async linting in Vim) |
47
+ | `rake` gem | For running tests during development |
48
+ | `rspec` gem | For running the test suite |
49
+
50
+ ### Supported Platforms
51
+
52
+ - macOS (Apple Silicon and Intel)
53
+ - Linux (x86_64, aarch64)
54
+ - Windows (via Ruby for Windows)
55
+
56
+ ---
57
+
58
+ ## Installation
59
+
60
+ ### From RubyGems (recommended)
61
+
62
+ ```bash
63
+ gem install openvox-lint
64
+ ```
65
+
66
+ ### From Source
67
+
68
+ ```bash
69
+ git clone https://github.com/cvquesty/openvox-lint.git
70
+ cd openvox-lint
71
+ gem build openvox-lint.gemspec
72
+ gem install openvox-lint-*.gem
73
+ ```
74
+
75
+ ### Via Bundler
76
+
77
+ Add to your `Gemfile`:
78
+
79
+ ```ruby
80
+ gem 'openvox-lint'
81
+ ```
82
+
83
+ Then run:
84
+
85
+ ```bash
86
+ bundle install
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Quick Start
92
+
93
+ ```bash
94
+ # Lint a single file
95
+ openvox-lint manifests/init.pp
96
+
97
+ # Lint all .pp files in a directory tree
98
+ openvox-lint manifests/
99
+
100
+ # Lint current directory
101
+ openvox-lint .
102
+
103
+ # List all available checks
104
+ openvox-lint --list-checks
105
+
106
+ # JSON output for CI
107
+ openvox-lint -f json manifests/
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Usage
113
+
114
+ ```
115
+ Usage: openvox-lint [options] [file|directory ...]
116
+
117
+ Options:
118
+ --version Display version
119
+ -f, --format FORMAT Output format: text, json, csv, github, codeclimate
120
+ --log-format FORMAT Custom log format string
121
+ --fix Automatically fix problems where possible
122
+ --fail-on-warnings Exit with error code on warnings
123
+ --no-filename Suppress filename in output
124
+ --no-column Suppress column number in output
125
+ --relative Display relative file paths
126
+ --only-checks CHECKS Comma-separated list of checks to run
127
+ --ignore-paths PATHS Comma-separated list of glob patterns to ignore
128
+ --list-checks List all available checks
129
+ -c, --config FILE Path to configuration file
130
+ --no-<check_name>-check Disable a specific check
131
+ ```
132
+
133
+ ### Examples
134
+
135
+ ```bash
136
+ # Disable specific checks
137
+ openvox-lint --no-line_length-check --no-arrow_alignment-check .
138
+
139
+ # Run only specific checks
140
+ openvox-lint --only-checks legacy_facts,hiera3_function,top_scope_facts .
141
+
142
+ # Custom log format (compatible with puppet-lint)
143
+ openvox-lint --log-format '%{path}:%{line}:%{column}:%{KIND}:%{check}:%{message}' .
144
+
145
+ # GitHub Actions annotations
146
+ openvox-lint -f github manifests/
147
+
148
+ # Fail CI on warnings too
149
+ openvox-lint --fail-on-warnings manifests/
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Built-in Checks
155
+
156
+ openvox-lint ships with **38 built-in checks** organized into categories:
157
+
158
+ ### Whitespace & Formatting (5 checks)
159
+
160
+ | Check | Severity | Description |
161
+ |-------|----------|-------------|
162
+ | `trailing_whitespace` | warning | No trailing whitespace at end of lines |
163
+ | `hard_tabs` | warning | Use 2-space soft tabs, not literal tabs |
164
+ | `line_length` | warning | Lines should not exceed 140 characters |
165
+ | `space_before_arrow` | warning | At most one space before `=>` |
166
+ | `strict_indent` | warning | Indentation must use 2-space increments |
167
+
168
+ ### Arrow Alignment (1 check)
169
+
170
+ | Check | Severity | Description |
171
+ |-------|----------|-------------|
172
+ | `arrow_alignment` | warning | `=>` arrows should align within resource bodies |
173
+
174
+ ### Quoting & Strings (5 checks)
175
+
176
+ | Check | Severity | Description |
177
+ |-------|----------|-------------|
178
+ | `double_quoted_strings` | warning | Use single quotes for strings without interpolation |
179
+ | `only_variable_string` | warning | Don't quote strings containing only a variable |
180
+ | `single_quote_string_with_variables` | warning | Use double quotes for strings with variables |
181
+ | `variables_not_enclosed` | warning | Variables in strings must use `${var}` braces |
182
+ | `quoted_booleans` | warning | Don't quote boolean values `true`/`false` |
183
+
184
+ ### Variables (2 checks)
185
+
186
+ | Check | Severity | Description |
187
+ |-------|----------|-------------|
188
+ | `variable_is_lowercase` | warning | All variables must be lowercase |
189
+ | `variable_contains_dash` | warning | Variables must not contain dashes |
190
+
191
+ ### Resources (7 checks)
192
+
193
+ | Check | Severity | Description |
194
+ |-------|----------|-------------|
195
+ | `ensure_first_param` | warning | `ensure` must be the first attribute |
196
+ | `ensure_not_symlink_target` | warning | Use `ensure => link` with `target` |
197
+ | `file_mode` | warning | File modes as 4-digit quoted octal or symbolic |
198
+ | `unquoted_file_mode` | warning | File modes must be quoted strings |
199
+ | `unquoted_resource_title` | warning | Resource titles must be quoted |
200
+ | `duplicate_params` | error | No duplicate parameters in resources |
201
+ | `trailing_comma` | warning | Trailing comma after last attribute |
202
+
203
+ ### Classes & Defines (5 checks)
204
+
205
+ | Check | Severity | Description |
206
+ |-------|----------|-------------|
207
+ | `documentation` | warning | Classes/defines must have preceding documentation |
208
+ | `nested_classes_or_defines` | warning | No nesting of classes or defines |
209
+ | `parameter_order` | warning | Params without defaults before params with defaults |
210
+ | `class_inherits_params` | warning | Avoid class inheritance; use composition |
211
+ | `inherits_across_namespaces` | warning | No inheritance across module namespaces |
212
+
213
+ ### Conditionals (2 checks)
214
+
215
+ | Check | Severity | Description |
216
+ |-------|----------|-------------|
217
+ | `case_without_default` | warning | Case statements must have a `default` case |
218
+ | `selector_inside_resource` | warning | Don't use selectors inside resource bodies |
219
+
220
+ ### References & Syntax (4 checks)
221
+
222
+ | Check | Severity | Description |
223
+ |-------|----------|-------------|
224
+ | `leading_zero` | warning | No leading zeros in numbers (except file modes) |
225
+ | `resource_reference_without_title_capital` | warning | Capitalise resource reference types |
226
+ | `relative_classname_inclusion` | warning | Use fully qualified class names |
227
+ | `autoloader_layout` | warning | Class/define name must match file path |
228
+
229
+ ### Comments (1 check)
230
+
231
+ | Check | Severity | Description |
232
+ |-------|----------|-------------|
233
+ | `star_comments` | warning | Use `#` comments, not `/* */` |
234
+
235
+ ### URLs (1 check)
236
+
237
+ | Check | Severity | Description |
238
+ |-------|----------|-------------|
239
+ | `puppet_url_without_modules` | warning | `puppet:///` URLs must include `/modules/` |
240
+
241
+ ### Nodes (1 check)
242
+
243
+ | Check | Severity | Description |
244
+ |-------|----------|-------------|
245
+ | `node_name_unquoted` | warning | Node names must be quoted strings |
246
+
247
+ ### Puppet 8 / OpenVox 8 Compatibility (4 checks) ⭐ NEW
248
+
249
+ | Check | Severity | Description |
250
+ |-------|----------|-------------|
251
+ | `legacy_facts` | warning | Legacy facts removed in Puppet 8; use `$facts['...']` |
252
+ | `top_scope_facts` | warning | `$::fact` references; use `$facts['fact']` |
253
+ | `hiera3_function` | **error** | `hiera()` / `hiera_hash()` removed; use `lookup()` |
254
+ | `import_statement` | **error** | `import` removed in Puppet 4+ |
255
+
256
+ ---
257
+
258
+ ## Configuration
259
+
260
+ ### RC File
261
+
262
+ Create `.openvox-lint.rc` in your project root or `~/.openvox-lint.rc`:
263
+
264
+ ```
265
+ # Disable specific checks
266
+ --no-line_length-check
267
+ --no-strict_indent-check
268
+
269
+ # Only run specific checks
270
+ # --only-checks legacy_facts,hiera3_function
271
+
272
+ # Fail on warnings in CI
273
+ --fail-on-warnings
274
+
275
+ # Ignore paths
276
+ --ignore-paths vendor/**/*.pp,pkg/**/*.pp
277
+ ```
278
+
279
+ ### Inline Ignore Comments
280
+
281
+ Suppress checks on specific lines:
282
+
283
+ ```puppet
284
+ # lint:ignore:line_length
285
+ $very_long_variable_name = 'a very long value that exceeds the line length limit because it is a very descriptive configuration string'
286
+ # lint:endignore
287
+
288
+ class foo { # lint:ignore:documentation
289
+ }
290
+ ```
291
+
292
+ ---
293
+
294
+ ## Output Formats
295
+
296
+ ### Text (default)
297
+
298
+ ```
299
+ manifests/init.pp:5:15: WARNING: unquoted_file_mode: unquoted file mode
300
+ manifests/init.pp:9:3: ERROR: hiera3_function: 'hiera()' is removed in Puppet 8
301
+ ```
302
+
303
+ ### JSON (`-f json`)
304
+
305
+ ```json
306
+ [
307
+ {
308
+ "path": "manifests/init.pp",
309
+ "line": 5,
310
+ "column": 15,
311
+ "kind": "warning",
312
+ "check": "unquoted_file_mode",
313
+ "message": "unquoted file mode"
314
+ }
315
+ ]
316
+ ```
317
+
318
+ ### GitHub Actions (`-f github`)
319
+
320
+ ```
321
+ ::warning file=manifests/init.pp,line=5,col=15::unquoted_file_mode: unquoted file mode
322
+ ```
323
+
324
+ ### CSV (`-f csv`)
325
+
326
+ ```
327
+ path,line,column,kind,check,message
328
+ manifests/init.pp,5,15,warning,unquoted_file_mode,"unquoted file mode"
329
+ ```
330
+
331
+ ### Code Climate (`-f codeclimate`)
332
+
333
+ Standard Code Climate JSON format for quality dashboards.
334
+
335
+ ### Custom (`--log-format`)
336
+
337
+ ```bash
338
+ openvox-lint --log-format '%{path}:%{line}:%{column}:%{KIND}:%{check}:%{message}' .
339
+ ```
340
+
341
+ Available placeholders: `%{path}`, `%{line}`, `%{column}`, `%{KIND}`, `%{kind}`, `%{check}`, `%{message}`
342
+
343
+ ---
344
+
345
+ ## Integration
346
+
347
+ ### vim-openvox
348
+
349
+ openvox-lint is the default backend for the [vim-openvox](https://github.com/cvquesty/vim-openvox) Vim plugin. Set in `.vimrc`:
350
+
351
+ ```vim
352
+ let g:openvox_lint_command = 'openvox-lint'
353
+ ```
354
+
355
+ ### GitHub Actions
356
+
357
+ ```yaml
358
+ name: Lint
359
+ on: [push, pull_request]
360
+ jobs:
361
+ lint:
362
+ runs-on: ubuntu-latest
363
+ steps:
364
+ - uses: actions/checkout@v4
365
+ - uses: ruby/setup-ruby@v1
366
+ with:
367
+ ruby-version: '3.2'
368
+ - run: gem install openvox-lint
369
+ - run: openvox-lint --fail-on-warnings -f github manifests/
370
+ ```
371
+
372
+ ### Rake Integration
373
+
374
+ ```ruby
375
+ require 'openvox-lint'
376
+
377
+ task :lint do
378
+ linter = OpenvoxLint::Linter.new
379
+ linter.run('manifests/')
380
+ OpenvoxLint::Report.new(OpenvoxLint.configuration).format(linter.problems)
381
+ exit linter.exit_code
382
+ end
383
+ ```
384
+
385
+ ### Pre-commit Hook
386
+
387
+ ```bash
388
+ #!/bin/bash
389
+ # .git/hooks/pre-commit
390
+ files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.pp$')
391
+ [ -z "$files" ] && exit 0
392
+ openvox-lint --fail-on-warnings $files
393
+ ```
394
+
395
+ ---
396
+
397
+ ## Writing Custom Checks
398
+
399
+ Create a Ruby file with a check plugin:
400
+
401
+ ```ruby
402
+ # lib/openvox-lint/plugins/checks/my_custom_check.rb
403
+ OpenvoxLint.new_check(:my_custom_check) do
404
+ def check
405
+ tokens.each do |tok|
406
+ if tok.type == :NAME && tok.value == 'something_bad'
407
+ notify :warning,
408
+ message: 'found something bad',
409
+ line: tok.line,
410
+ column: tok.column
411
+ end
412
+ end
413
+ end
414
+ end
415
+ ```
416
+
417
+ Place in `lib/openvox-lint/plugins/checks/` and it will be auto-loaded.
418
+
419
+ ---
420
+
421
+ ## Development
422
+
423
+ ```bash
424
+ git clone https://github.com/cvquesty/openvox-lint.git
425
+ cd openvox-lint
426
+ bundle install
427
+ bundle exec rake spec
428
+ ```
429
+
430
+ ### Project Structure
431
+
432
+ ```
433
+ openvox-lint/
434
+ ├── bin/
435
+ │ └── openvox-lint # CLI executable
436
+ ├── lib/
437
+ │ ├── openvox-lint.rb # Main entry point
438
+ │ └── openvox-lint/
439
+ │ ├── version.rb # Version constant
440
+ │ ├── configuration.rb # Configuration management
441
+ │ ├── token.rb # Token data structure
442
+ │ ├── lexer.rb # Puppet/OpenVox lexer/tokenizer
443
+ │ ├── check_plugin.rb # Base class for checks
444
+ │ ├── checks.rb # Check runner/orchestrator
445
+ │ ├── report.rb # Output formatters
446
+ │ ├── linter.rb # File-level orchestrator
447
+ │ ├── cli.rb # Command-line interface
448
+ │ └── plugins/
449
+ │ └── checks/ # 38 built-in check plugins
450
+ │ ├── trailing_whitespace.rb
451
+ │ ├── legacy_facts.rb
452
+ │ ├── hiera3_function.rb
453
+ │ └── ...
454
+ ├── spec/ # RSpec test suite
455
+ ├── openvox-lint.gemspec
456
+ ├── Gemfile
457
+ ├── Rakefile
458
+ ├── LICENSE # Apache 2.0
459
+ ├── README.md
460
+ ├── CHANGELOG.md
461
+ └── DOCUMENTATION.md
462
+ ```
463
+
464
+ ---
465
+
466
+ ## Comparison with puppet-lint
467
+
468
+ | Feature | puppet-lint 5.x | openvox-lint 1.0 |
469
+ |---------|-----------------|------------------|
470
+ | Ruby requirement | ≥ 3.1 | ≥ 3.1 |
471
+ | Runtime dependencies | None | None |
472
+ | Built-in checks | ~25 | 38 |
473
+ | Legacy facts detection | Via plugin | Built-in |
474
+ | Top-scope facts detection | Via plugin | Built-in |
475
+ | Hiera 3 detection | No | Built-in (error) |
476
+ | Import statement detection | No | Built-in (error) |
477
+ | Strict indent check | Via plugin | Built-in |
478
+ | GitHub Actions output | No | Built-in (`-f github`) |
479
+ | Code Climate output | No | Built-in (`-f codeclimate`) |
480
+ | CSV output | No | Built-in (`-f csv`) |
481
+ | Custom log format | Yes | Yes (compatible) |
482
+ | OpenVox awareness | No | Yes |
483
+ | Plugin system | Yes | Yes (compatible) |
484
+ | `--fix` support | Yes | Yes |
485
+ | vim-openvox integration | No | Native |
486
+
487
+ ---
488
+
489
+ ## License
490
+
491
+ Apache License, Version 2.0. See [LICENSE](LICENSE) for details.
492
+
493
+ ---
494
+
495
+ ## Author
496
+
497
+ Johnny Sheets ([@cvquesty](https://github.com/cvquesty))
data/bin/openvox-lint ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/openvox-lint'
5
+
6
+ exit_code = OpenvoxLint::CLI.new(ARGV.dup).run
7
+ exit exit_code
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenvoxLint
4
+ # Base class for every lint check. Subclass via OpenvoxLint.new_check.
5
+ class CheckPlugin
6
+ attr_reader :problems
7
+
8
+ class << self
9
+ attr_reader :check_name
10
+ end
11
+
12
+ def initialize
13
+ @problems = []
14
+ end
15
+
16
+ def run(tokens:, manifest_lines:, fullpath:, ignore_comments: [])
17
+ @tokens = tokens
18
+ @manifest_lines = manifest_lines
19
+ @fullpath = fullpath
20
+ @path = fullpath
21
+ @filename = File.basename(fullpath)
22
+ @problems = []
23
+ @ignore_comments = ignore_comments
24
+ check
25
+ @problems.reject! { |p| ignored?(p) }
26
+ @problems
27
+ end
28
+
29
+ def fix_problems
30
+ @problems.each do |problem|
31
+ fix(problem)
32
+ rescue OpenvoxLint::NoFix
33
+ next
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ def check
40
+ raise NotImplementedError, "#{self.class} must implement #check"
41
+ end
42
+
43
+ def fix(_problem)
44
+ raise OpenvoxLint::NoFix
45
+ end
46
+
47
+ attr_reader :tokens, :manifest_lines, :fullpath, :path, :filename
48
+
49
+ def notify(kind, details)
50
+ details[:kind] = kind
51
+ details[:check] = self.class.check_name
52
+ details[:path] = @path
53
+ @problems << details
54
+ end
55
+
56
+ def semantic_tokens
57
+ tokens.reject(&:formatting?)
58
+ end
59
+
60
+ def resource_indexes
61
+ @resource_indexes ||= compute_resource_indexes
62
+ end
63
+
64
+ def class_indexes
65
+ @class_indexes ||= find_keyword_indexes(:CLASS)
66
+ end
67
+
68
+ def defined_type_indexes
69
+ @defined_type_indexes ||= find_keyword_indexes(:DEFINE)
70
+ end
71
+
72
+ def node_indexes
73
+ @node_indexes ||= find_keyword_indexes(:NODE)
74
+ end
75
+
76
+ def title_tokens
77
+ @title_tokens ||= compute_title_tokens
78
+ end
79
+
80
+ private
81
+
82
+ def ignored?(problem)
83
+ return false if @ignore_comments.empty?
84
+ line = problem[:line]
85
+ @ignore_comments.any? do |ic|
86
+ ic[:line] == line && (ic[:checks].empty? || ic[:checks].include?(problem[:check].to_s))
87
+ end
88
+ end
89
+
90
+ def compute_resource_indexes
91
+ results = []; i = 0; sem = semantic_tokens
92
+ while i < sem.length
93
+ if sem[i].type == :NAME && i + 1 < sem.length && sem[i + 1].type == :LBRACE
94
+ rtype = sem[i]; brace = i + 1; depth = 1; j = brace + 1; params = []
95
+ while j < sem.length && depth > 0
96
+ case sem[j].type
97
+ when :LBRACE then depth += 1
98
+ when :RBRACE then depth -= 1
99
+ end
100
+ params << sem[j] if depth > 0
101
+ j += 1
102
+ end
103
+ results << { type: rtype, start: brace, end: j - 1, param_tokens: params }
104
+ i = j
105
+ else
106
+ i += 1
107
+ end
108
+ end
109
+ results
110
+ end
111
+
112
+ def find_keyword_indexes(keyword)
113
+ results = []; sem = semantic_tokens
114
+ sem.each_with_index do |tok, i|
115
+ next unless tok.type == keyword
116
+ j = i + 1
117
+ j += 1 while j < sem.length && sem[j].type != :LBRACE
118
+ next if j >= sem.length
119
+ brace_start = j; depth = 1; j += 1
120
+ while j < sem.length && depth > 0
121
+ case sem[j].type
122
+ when :LBRACE then depth += 1
123
+ when :RBRACE then depth -= 1
124
+ end
125
+ j += 1
126
+ end
127
+ params = sem[(i + 1)...brace_start]
128
+ results << {
129
+ start: i, end: j - 1, tokens: sem[i...j],
130
+ param_tokens: params, name_token: sem[i + 1],
131
+ }
132
+ end
133
+ results
134
+ end
135
+
136
+ def compute_title_tokens
137
+ results = []; sem = semantic_tokens
138
+ sem.each_with_index do |tok, i|
139
+ if tok.type == :LBRACE && i > 0 && sem[i - 1].type == :NAME
140
+ j = i + 1
141
+ results << sem[j] if j < sem.length && %i[SSTRING STRING NAME VARIABLE].include?(sem[j].type)
142
+ end
143
+ end
144
+ results
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenvoxLint
4
+ # Runs all enabled checks against a tokenised manifest.
5
+ class Checks
6
+ attr_reader :problems
7
+
8
+ def initialize(tokens:, manifest_lines:, fullpath:, configuration:)
9
+ @tokens = tokens
10
+ @manifest_lines = manifest_lines
11
+ @fullpath = fullpath
12
+ @configuration = configuration
13
+ @problems = []
14
+ @ignore_comments = parse_ignore_comments
15
+ end
16
+
17
+ def run
18
+ OpenvoxLint.checks.each do |name, klass|
19
+ next unless @configuration.check_enabled?(name)
20
+ plugin = klass.new
21
+ results = plugin.run(
22
+ tokens: @tokens, manifest_lines: @manifest_lines,
23
+ fullpath: @fullpath, ignore_comments: @ignore_comments,
24
+ )
25
+ @problems.concat(results)
26
+ plugin.fix_problems if @configuration.fix && plugin.respond_to?(:fix_problems)
27
+ end
28
+ @problems.sort_by { |p| [p[:line] || 0, p[:column] || 0] }
29
+ end
30
+
31
+ private
32
+
33
+ def parse_ignore_comments
34
+ results = []
35
+ @tokens.each do |tok|
36
+ next unless tok.type == :COMMENT
37
+ if tok.value =~ /lint:ignore:(.+)/
38
+ results << { line: tok.line, checks: Regexp.last_match(1).strip.split(/\s*,\s*/) }
39
+ elsif tok.value =~ /lint:ignore\b/
40
+ results << { line: tok.line, checks: [] }
41
+ end
42
+ end
43
+ results
44
+ end
45
+ end
46
+ end