rubocop-yaml 0.1.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 +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE +21 -0
- data/README.md +123 -0
- data/config/default.yml +59 -0
- data/config/schemas/compose.json +29 -0
- data/config/schemas/github-actions.json +17 -0
- data/docs/README.md +21 -0
- data/docs/releasing.md +35 -0
- data/lib/rubocop/cop/yaml/base.rb +27 -0
- data/lib/rubocop/cop/yaml/lint/duplicate_key.rb +54 -0
- data/lib/rubocop/cop/yaml/lint/invalid_syntax.rb +26 -0
- data/lib/rubocop/cop/yaml/rails/database_environment_consistency.rb +84 -0
- data/lib/rubocop/cop/yaml/schema/validation.rb +51 -0
- data/lib/rubocop/cop/yaml/style/key_grouping.rb +93 -0
- data/lib/rubocop/cop/yaml/style/key_ordering.rb +67 -0
- data/lib/rubocop/yaml/parser.rb +64 -0
- data/lib/rubocop/yaml/plugin.rb +34 -0
- data/lib/rubocop/yaml/processed_source.rb +25 -0
- data/lib/rubocop/yaml/schema/registry.rb +31 -0
- data/lib/rubocop/yaml/schema/validator.rb +131 -0
- data/lib/rubocop/yaml/version.rb +8 -0
- data/lib/rubocop/yaml.rb +17 -0
- data/sig/rubocop/yaml/parser.rbs +38 -0
- metadata +165 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e7da8d1f4ee4acbde7506fbfd99365f82f83a3b78fd9dddf4125f30ee3afedd0
|
|
4
|
+
data.tar.gz: 9bc6713ac5b29837b1b78f0ae296f4f29900b564275c7624d124b8edf7946a2a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c6ebd15cec006677d80ff88ce2ba97e8ed00deaaaf2b8d367d1d3e428807f77222540c42fa8c3d74dfe2bc1ed8aeb5558d45f65ee2fc8e8da9ce23f22e440b1c
|
|
7
|
+
data.tar.gz: 131958b6d72cdb5ca6f33e8e47ea6849ce671b9869093a382bad88329adcb2de96261a13983c46f0354ac4925123f4c0e27859664dc1fb3b3d20dde7fa6f4c62
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!-- CHANGELOG.md -->
|
|
2
|
+
# Changelog
|
|
3
|
+
|
|
4
|
+
## Unreleased
|
|
5
|
+
|
|
6
|
+
## 0.1.0 - 2026-09-13
|
|
7
|
+
|
|
8
|
+
- Establish the RuboCop plugin foundation.
|
|
9
|
+
- Add standardized local development commands and CI.
|
|
10
|
+
- Add a safe YAML parser abstraction with immutable nodes and source locations.
|
|
11
|
+
- Add `YAML/Lint/InvalidSyntax`.
|
|
12
|
+
- Add `YAML/Lint/DuplicateKey`.
|
|
13
|
+
- Add opt-in `YAML/Style/KeyOrdering`.
|
|
14
|
+
- Add opt-in `YAML/Style/KeyGrouping`.
|
|
15
|
+
- Add opt-in local JSON Schema validation.
|
|
16
|
+
- Add deterministic Compose and GitHub Actions schema autodetection.
|
|
17
|
+
- Add Rails database environment consistency checks.
|
|
18
|
+
|
|
19
|
+
—
|
|
20
|
+
Stan Carver II
|
|
21
|
+
Made in Texas 🤠
|
|
22
|
+
https://stancarver.com
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stan Carver II
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<!-- README.md -->
|
|
2
|
+
# RuboCop YAML
|
|
3
|
+
|
|
4
|
+
RuboCop YAML brings deterministic YAML syntax, style, schema, and Rails-configuration checks into RuboCop while preserving comments, anchors, aliases, and document structure.
|
|
5
|
+
|
|
6
|
+
## Status
|
|
7
|
+
|
|
8
|
+
Version 0.1.0 is the first public release. Its existing semantics are the baseline for real-world dogfooding; new behavior should be driven by confirmed usage rather than speculative fixtures.
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
- Ruby 3.2 or newer
|
|
13
|
+
- RuboCop 1.72 or newer and earlier than 2.0
|
|
14
|
+
|
|
15
|
+
The gem is framework-independent. Rails is not a runtime dependency; Rails-specific cops operate on configuration files by path and structure.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Add the gem to the development and test groups in your `Gemfile`:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
group :development, :test do
|
|
23
|
+
gem "rubocop-yaml", "~> 0.1", require: false
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Run `bundle install`, then register the plugin in `.rubocop.yml`:
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
plugins:
|
|
31
|
+
- rubocop-yaml
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
RuboCop YAML automatically includes `**/*.yml` and `**/*.yaml` files.
|
|
35
|
+
|
|
36
|
+
## Cops
|
|
37
|
+
|
|
38
|
+
| Cop | Default | Purpose |
|
|
39
|
+
| --- | ---: | --- |
|
|
40
|
+
| `YAML/Lint/InvalidSyntax` | Enabled | Reports malformed YAML at the parser location |
|
|
41
|
+
| `YAML/Lint/DuplicateKey` | Enabled | Reports silent duplicate scalar keys |
|
|
42
|
+
| `YAML/Style/KeyOrdering` | Disabled | Enforces alphabetical or configured key order |
|
|
43
|
+
| `YAML/Style/KeyGrouping` | Disabled | Enforces semantic groups and optional spacing |
|
|
44
|
+
| `YAML/Schema/Validation` | Disabled | Validates against local or autodetected JSON Schemas |
|
|
45
|
+
| `YAML/Rails/DatabaseEnvironmentConsistency` | Enabled for `config/database.yml` | Reports missing Rails environments and required keys |
|
|
46
|
+
|
|
47
|
+
## Configuration examples
|
|
48
|
+
|
|
49
|
+
Configure explicit key order without enabling autocorrection:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
YAML/Style/KeyOrdering:
|
|
53
|
+
Enabled: true
|
|
54
|
+
EnforcedStyle: configured
|
|
55
|
+
Keys:
|
|
56
|
+
- name
|
|
57
|
+
- image
|
|
58
|
+
- environment
|
|
59
|
+
- volumes
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Map project files to local JSON Schemas:
|
|
63
|
+
|
|
64
|
+
```yaml
|
|
65
|
+
YAML/Schema/Validation:
|
|
66
|
+
Enabled: true
|
|
67
|
+
Schemas:
|
|
68
|
+
"config/services/*.yml": "schemas/service.json"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
With `AutoDetect: true`, schema validation recognizes Compose files and `.github/workflows/*.yml` using pinned, vendored schemas. Explicit `Schemas` mappings take precedence.
|
|
72
|
+
|
|
73
|
+
Customize Rails database expectations when an application uses nonstandard environments or inherited keys:
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
YAML/Rails/DatabaseEnvironmentConsistency:
|
|
77
|
+
RequiredEnvironments:
|
|
78
|
+
- development
|
|
79
|
+
- test
|
|
80
|
+
- staging
|
|
81
|
+
- production
|
|
82
|
+
RequiredKeys:
|
|
83
|
+
- adapter
|
|
84
|
+
- database
|
|
85
|
+
AllowMergeKeys: true
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Known limitations
|
|
89
|
+
|
|
90
|
+
- No cop autocorrects YAML in 0.1.0. Rewriting YAML safely across comments, aliases, anchors, merge keys, and ordering semantics requires dedicated research.
|
|
91
|
+
- Schema validation never retrieves remote references implicitly. Use local, reviewed schemas.
|
|
92
|
+
- The schema registry currently autodetects only Docker Compose and GitHub Actions workflows.
|
|
93
|
+
- The Rails database cop analyzes YAML statically. ERB-looking content is preserved as scalar text and never executed.
|
|
94
|
+
- Ordering and grouping cops skip mappings with complex or merge keys when enforcing an order could misrepresent YAML semantics.
|
|
95
|
+
- Parsing follows the Psych behavior supplied by the active Ruby version; parser edge cases will be expanded through dogfooding and a dedicated torture suite.
|
|
96
|
+
|
|
97
|
+
## Development and verification
|
|
98
|
+
|
|
99
|
+
Run `bin/setup`, then `bin/ci`. Use `bin/rspec` and `bin/rubocop` with passthrough arguments for focused checks.
|
|
100
|
+
|
|
101
|
+
`bin/package` builds the gem and verifies that runtime configuration, vendored schemas, Ruby code, and RBS signatures are present. `bin/e2e` installs that artifact into an isolated gem home and proves RuboCop plugin discovery against a Rails-shaped fixture outside the repository load path.
|
|
102
|
+
|
|
103
|
+
See the [documentation index](docs/README.md) for architecture notes and the [release runbook](docs/releasing.md) for trusted-publishing setup and release verification.
|
|
104
|
+
|
|
105
|
+
## Project layout
|
|
106
|
+
|
|
107
|
+
- `lib/` contains the plugin, cops, and public API.
|
|
108
|
+
- `config/` contains default RuboCop configuration and pinned schemas.
|
|
109
|
+
- `sig/` contains public RBS contracts.
|
|
110
|
+
- `spec/` contains behavior, integration, and coverage checks.
|
|
111
|
+
- `docs/` contains architecture and release documentation.
|
|
112
|
+
- `bin/` contains repeatable development, package, and external-fixture commands.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
RuboCop YAML is available under the [MIT License](LICENSE).
|
|
117
|
+
|
|
118
|
+
Source: [github.com/scarver2/rubocop-yaml](https://github.com/scarver2/rubocop-yaml)
|
|
119
|
+
|
|
120
|
+
—
|
|
121
|
+
Stan Carver II
|
|
122
|
+
Made in Texas 🤠
|
|
123
|
+
https://stancarver.com
|
data/config/default.yml
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# config/default.yml
|
|
2
|
+
AllCops:
|
|
3
|
+
Include:
|
|
4
|
+
- "**/*.yaml"
|
|
5
|
+
- "**/*.yml"
|
|
6
|
+
|
|
7
|
+
YAML/Lint/InvalidSyntax:
|
|
8
|
+
Description: Reports malformed YAML using the parser's best source location.
|
|
9
|
+
Enabled: true
|
|
10
|
+
VersionAdded: "0.1"
|
|
11
|
+
|
|
12
|
+
YAML/Lint/DuplicateKey:
|
|
13
|
+
Description: Reports repeated scalar keys within the same YAML mapping.
|
|
14
|
+
Enabled: true
|
|
15
|
+
VersionAdded: "0.1"
|
|
16
|
+
|
|
17
|
+
YAML/Style/KeyOrdering:
|
|
18
|
+
Description: Enforces alphabetical or explicitly configured mapping-key order.
|
|
19
|
+
Enabled: false
|
|
20
|
+
EnforcedStyle: alphabetical
|
|
21
|
+
SupportedStyles:
|
|
22
|
+
- alphabetical
|
|
23
|
+
- configured
|
|
24
|
+
Keys: []
|
|
25
|
+
VersionAdded: "0.1"
|
|
26
|
+
|
|
27
|
+
YAML/Style/KeyGrouping:
|
|
28
|
+
Description: Groups semantically related YAML keys in a configured order.
|
|
29
|
+
Enabled: false
|
|
30
|
+
Groups: []
|
|
31
|
+
Scopes:
|
|
32
|
+
- "*"
|
|
33
|
+
UnknownKeys: last
|
|
34
|
+
RequireBlankLines: false
|
|
35
|
+
VersionAdded: "0.1"
|
|
36
|
+
|
|
37
|
+
YAML/Schema/Validation:
|
|
38
|
+
Description: Validates YAML documents against configured local JSON Schemas.
|
|
39
|
+
Enabled: false
|
|
40
|
+
AutoDetect: true
|
|
41
|
+
Debug: false
|
|
42
|
+
Schemas: {}
|
|
43
|
+
VersionAdded: "0.1"
|
|
44
|
+
|
|
45
|
+
YAML/Rails/DatabaseEnvironmentConsistency:
|
|
46
|
+
Description: Requires consistent environments and keys in Rails database configuration.
|
|
47
|
+
Enabled: true
|
|
48
|
+
Include:
|
|
49
|
+
- "**/config/database.yml"
|
|
50
|
+
- "**/config/database.yaml"
|
|
51
|
+
RequiredEnvironments:
|
|
52
|
+
- development
|
|
53
|
+
- test
|
|
54
|
+
- production
|
|
55
|
+
RequiredKeys:
|
|
56
|
+
- adapter
|
|
57
|
+
- database
|
|
58
|
+
AllowMergeKeys: true
|
|
59
|
+
VersionAdded: "0.1"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://rubocop-yaml.dev/schemas/compose-spec-2026-08",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["services"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"name": { "type": "string" },
|
|
8
|
+
"services": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"additionalProperties": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"properties": {
|
|
13
|
+
"image": { "type": "string" },
|
|
14
|
+
"build": { "type": ["string", "object"] },
|
|
15
|
+
"command": { "type": ["string", "array"] },
|
|
16
|
+
"environment": { "type": ["object", "array"] },
|
|
17
|
+
"ports": { "type": "array" },
|
|
18
|
+
"volumes": { "type": "array" },
|
|
19
|
+
"depends_on": { "type": ["object", "array"] }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"networks": { "type": "object" },
|
|
24
|
+
"volumes": { "type": "object" },
|
|
25
|
+
"configs": { "type": "object" },
|
|
26
|
+
"secrets": { "type": "object" }
|
|
27
|
+
},
|
|
28
|
+
"additionalProperties": false
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://rubocop-yaml.dev/schemas/github-actions-schema-store-2026-08",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["on", "jobs"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"name": { "type": "string" },
|
|
8
|
+
"run-name": { "type": "string" },
|
|
9
|
+
"on": { "type": ["string", "array", "object"] },
|
|
10
|
+
"permissions": { "type": ["string", "object"] },
|
|
11
|
+
"env": { "type": "object" },
|
|
12
|
+
"defaults": { "type": "object" },
|
|
13
|
+
"concurrency": { "type": ["string", "object"] },
|
|
14
|
+
"jobs": { "type": "object", "minProperties": 1 }
|
|
15
|
+
},
|
|
16
|
+
"additionalProperties": false
|
|
17
|
+
}
|
data/docs/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!-- docs/README.md -->
|
|
2
|
+
# Documentation
|
|
3
|
+
|
|
4
|
+
- The parser layer owns safe YAML parsing and source locations.
|
|
5
|
+
- Cops own policy and native RuboCop offense presentation.
|
|
6
|
+
- Schema checks never retrieve remote resources implicitly.
|
|
7
|
+
- `YAML/Style/KeyOrdering` ignores unknown keys in configured mode and skips mappings containing merge or complex keys. It intentionally has no autocorrection so comments and YAML semantics remain untouched.
|
|
8
|
+
- `YAML/Style/KeyGrouping` accepts named key groups, dot-separated nested scopes, `last` or `ignore` handling for unknown keys, and optional blank-line separation. It does not autocorrect.
|
|
9
|
+
- `YAML/Schema/Validation` maps project-relative file globs to local JSON Schema paths. Schemas are cached per run, local references are supported, and remote references are rejected by the validator's default resolver.
|
|
10
|
+
- Schema autodetection recognizes Compose files and `.github/workflows/*.yml`. Explicit mappings take precedence, `AutoDetect: false` disables the registry, and `Debug: true` prints the selected pinned schema identity.
|
|
11
|
+
- `YAML/Rails/DatabaseEnvironmentConsistency` statically checks `config/database.yml` for required environments and keys, including keys inherited through anchors and merge aliases. Configure `RequiredEnvironments`, `RequiredKeys`, or `AllowMergeKeys` to avoid false positives in nonstandard deployments. ERB is preserved as scalar text and is never executed.
|
|
12
|
+
|
|
13
|
+
## Further reading
|
|
14
|
+
|
|
15
|
+
- Development and testing instructions live in the project [README](../README.md).
|
|
16
|
+
- [Releasing](releasing.md) documents package verification, trusted publishing, and rollback boundaries.
|
|
17
|
+
|
|
18
|
+
—
|
|
19
|
+
Stan Carver II
|
|
20
|
+
Made in Texas 🤠
|
|
21
|
+
https://stancarver.com
|
data/docs/releasing.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<!-- docs/releasing.md -->
|
|
2
|
+
# Releasing RuboCop YAML
|
|
3
|
+
|
|
4
|
+
RuboCop YAML publishes from GitHub Actions with RubyGems trusted publishing. The release workflow accepts tags matching `v*`, verifies that the tag matches `RuboCop::Yaml::VERSION`, runs the complete project checks, installs the packaged gem into an isolated Rails-shaped fixture, and then invokes the official RubyGems release action.
|
|
5
|
+
|
|
6
|
+
## One-time trusted publisher setup
|
|
7
|
+
|
|
8
|
+
Create a pending trusted publisher in the RubyGems account that will own `rubocop-yaml`:
|
|
9
|
+
|
|
10
|
+
- Gem name: `rubocop-yaml`
|
|
11
|
+
- Repository owner: `scarver2`
|
|
12
|
+
- Repository name: `rubocop-yaml`
|
|
13
|
+
- Workflow filename: `release.yml`
|
|
14
|
+
- GitHub environment: `release`
|
|
15
|
+
|
|
16
|
+
The repository must also have a GitHub Actions environment named `release`. Configure required reviewers there when an explicit deployment approval is desired. No long-lived RubyGems API key belongs in GitHub secrets.
|
|
17
|
+
|
|
18
|
+
## Release checklist
|
|
19
|
+
|
|
20
|
+
1. Confirm the release commit is on `master` and the worktree is clean.
|
|
21
|
+
2. Confirm `CHANGELOG.md` has a dated entry matching the version.
|
|
22
|
+
3. Run `bin/ci` to execute specs, coverage, RuboCop, RBS validation, and package inspection.
|
|
23
|
+
4. Run `bin/e2e` to install the built gem in an isolated gem home and lint a Rails-shaped fixture.
|
|
24
|
+
5. Create and push the signed release tag, such as `v0.1.0`.
|
|
25
|
+
6. Approve the `release` environment deployment if protection rules require it.
|
|
26
|
+
7. Verify the GitHub Actions release job, the RubyGems version, and the generated GitHub release/tag page.
|
|
27
|
+
|
|
28
|
+
## Failure and rollback boundaries
|
|
29
|
+
|
|
30
|
+
Do not move or reuse a published version tag. If verification fails before publication, fix the release branch and create the tag only after the corrected commit reaches `master`. RubyGems releases are immutable; if a defective version is published, document the defect, release a new patch version, and yank only when leaving the version available would materially harm users.
|
|
31
|
+
|
|
32
|
+
—
|
|
33
|
+
Stan Carver II
|
|
34
|
+
Made in Texas 🤠
|
|
35
|
+
https://stancarver.com
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# lib/rubocop/cop/yaml/base.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "rubocop"
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module Cop
|
|
8
|
+
module YAML
|
|
9
|
+
class Base < RuboCop::Cop::Base
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def yaml_result
|
|
13
|
+
@yaml_result ||= RuboCop::Yaml::Parser.new.parse(
|
|
14
|
+
processed_source.raw_source,
|
|
15
|
+
path: processed_source.path
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def source_range(line, column, length = 1)
|
|
20
|
+
buffer = processed_source.buffer
|
|
21
|
+
begin_pos = buffer.line_range(line).begin_pos + column - 1
|
|
22
|
+
Parser::Source::Range.new(buffer, begin_pos, begin_pos + length)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# lib/rubocop/cop/yaml/lint/duplicate_key.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../base"
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module Cop
|
|
8
|
+
module YAML
|
|
9
|
+
module Lint
|
|
10
|
+
class DuplicateKey < Base
|
|
11
|
+
MSG = "Duplicate key `%<key>s`; first declared on line %<line>d."
|
|
12
|
+
|
|
13
|
+
def on_new_investigation # rubocop:disable Metrics/AbcSize
|
|
14
|
+
return unless yaml_result.success?
|
|
15
|
+
|
|
16
|
+
duplicates(yaml_result.stream).each do |key, first|
|
|
17
|
+
add_offense(
|
|
18
|
+
source_range(key.location.line, key.location.column, key.value.length),
|
|
19
|
+
message: format(MSG, key: key.value, line: first.location.line)
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def duplicates(root)
|
|
25
|
+
find_mappings(root).flat_map { |mapping| mapping_duplicates(mapping) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def find_mappings(node)
|
|
31
|
+
matches = node.type == :mapping ? [node] : []
|
|
32
|
+
matches + node.children.flat_map { |child| find_mappings(child) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def mapping_duplicates(mapping)
|
|
36
|
+
seen = {}
|
|
37
|
+
mapping.children.each_slice(2).filter_map do |key, _value|
|
|
38
|
+
next unless scalar_key?(key)
|
|
39
|
+
|
|
40
|
+
identity = [key.tag, key.value]
|
|
41
|
+
first = seen[identity]
|
|
42
|
+
seen[identity] ||= key
|
|
43
|
+
[key, first] if first
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def scalar_key?(key)
|
|
48
|
+
key&.type == :scalar && key.value != "<<"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# lib/rubocop/cop/yaml/lint/invalid_syntax.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../base"
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module Cop
|
|
8
|
+
module YAML
|
|
9
|
+
module Lint
|
|
10
|
+
class InvalidSyntax < Base
|
|
11
|
+
MSG = "Invalid YAML syntax: %<message>s"
|
|
12
|
+
|
|
13
|
+
def on_new_investigation
|
|
14
|
+
diagnostic = yaml_result.diagnostic
|
|
15
|
+
return unless diagnostic
|
|
16
|
+
|
|
17
|
+
add_offense(
|
|
18
|
+
source_range(diagnostic.line, diagnostic.column),
|
|
19
|
+
message: format(MSG, message: diagnostic.message)
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# lib/rubocop/cop/yaml/rails/database_environment_consistency.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../base"
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module Cop
|
|
8
|
+
module YAML
|
|
9
|
+
module Rails
|
|
10
|
+
class DatabaseEnvironmentConsistency < Base
|
|
11
|
+
Finding = Struct.new(:node, :message, keyword_init: true)
|
|
12
|
+
MISSING_ENV = "Define the `%<environment>s` database environment."
|
|
13
|
+
MISSING_KEY = "Database environment `%<environment>s` is missing required key `%<key>s`."
|
|
14
|
+
|
|
15
|
+
def on_new_investigation
|
|
16
|
+
return unless database_config? && yaml_result.success?
|
|
17
|
+
|
|
18
|
+
configured_findings.each do |finding|
|
|
19
|
+
node = finding.node
|
|
20
|
+
add_offense(source_range(node.location.line, node.location.column, node.value&.length || 1), message: finding.message)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def findings(stream, required_environments:, required_keys:, allow_merge_keys: true)
|
|
25
|
+
root = stream.children.first&.children&.first
|
|
26
|
+
return [] unless root&.type == :mapping
|
|
27
|
+
|
|
28
|
+
entries = root.children.each_slice(2).to_h { |key, value| [key.value, [key, value]] }
|
|
29
|
+
anchors = anchor_mappings(root)
|
|
30
|
+
required_environments.flat_map do |environment|
|
|
31
|
+
environment_findings(root, entries, anchors, environment, required_keys, allow_merge_keys)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def database_config?
|
|
38
|
+
File.basename(processed_source.path).match?(/\Adatabase\.ya?ml\z/)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def configured_findings
|
|
42
|
+
findings(
|
|
43
|
+
yaml_result.stream,
|
|
44
|
+
required_environments: cop_config.fetch("RequiredEnvironments", %w[development test production]),
|
|
45
|
+
required_keys: cop_config.fetch("RequiredKeys", %w[adapter database]),
|
|
46
|
+
allow_merge_keys: cop_config.fetch("AllowMergeKeys", true)
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def environment_findings(root, entries, anchors, environment, required_keys, allow_merge_keys) # rubocop:disable Metrics/ParameterLists
|
|
51
|
+
key, mapping = entries[environment]
|
|
52
|
+
return [Finding.new(node: root, message: format(MISSING_ENV, environment: environment))] unless key
|
|
53
|
+
return required_keys.map { |required| missing_key(key, environment, required) } unless mapping.type == :mapping
|
|
54
|
+
|
|
55
|
+
present = effective_keys(mapping, anchors, allow_merge_keys)
|
|
56
|
+
required_keys.filter_map { |required| missing_key(key, environment, required) unless present.include?(required) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def missing_key(node, environment, key)
|
|
60
|
+
Finding.new(node: node, message: format(MISSING_KEY, environment: environment, key: key))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def anchor_mappings(node)
|
|
64
|
+
own = node.type == :mapping && node.anchor ? { node.anchor => node } : {}
|
|
65
|
+
node.children.each_with_object(own) { |child, anchors| anchors.merge!(anchor_mappings(child)) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def effective_keys(mapping, anchors, allow_merge_keys)
|
|
69
|
+
keys = Set.new
|
|
70
|
+
mapping.children.each_slice(2) do |key, value|
|
|
71
|
+
if key.value == "<<" && allow_merge_keys
|
|
72
|
+
merged = anchors[value.anchor]
|
|
73
|
+
keys.merge(effective_keys(merged, anchors, false)) if merged
|
|
74
|
+
else
|
|
75
|
+
keys << key.value
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
keys
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# lib/rubocop/cop/yaml/schema/validation.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../base"
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module Cop
|
|
8
|
+
module YAML
|
|
9
|
+
module Schema
|
|
10
|
+
class Validation < Base
|
|
11
|
+
MSG = "Schema violation at `%<pointer>s`: %<message>s"
|
|
12
|
+
|
|
13
|
+
def on_new_investigation
|
|
14
|
+
findings.each do |finding|
|
|
15
|
+
add_offense(
|
|
16
|
+
source_range(finding.line, finding.column),
|
|
17
|
+
message: format(MSG, pointer: finding.pointer.empty? ? "/" : finding.pointer, message: finding.message)
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
debug_schema
|
|
21
|
+
rescue RuboCop::Yaml::Schema::Validator::ConfigurationError => e
|
|
22
|
+
add_offense(source_range(1, 1), message: "Schema configuration error: #{e.message}")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def findings
|
|
28
|
+
validator.validate(
|
|
29
|
+
source: processed_source.raw_source,
|
|
30
|
+
path: processed_source.path,
|
|
31
|
+
schemas: cop_config.fetch("Schemas", {}),
|
|
32
|
+
root: Dir.pwd,
|
|
33
|
+
autodetect: cop_config.fetch("AutoDetect", true)
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def validator
|
|
38
|
+
@validator ||= RuboCop::Yaml::Schema::Validator.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def debug_schema
|
|
42
|
+
return unless cop_config.fetch("Debug", false) && validator.selected_schema
|
|
43
|
+
|
|
44
|
+
entry = validator.selected_schema
|
|
45
|
+
warn("rubocop-yaml: #{processed_source.path} uses #{entry.name} schema #{entry.version} (#{entry.path})")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# lib/rubocop/cop/yaml/style/key_grouping.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../base"
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module Cop
|
|
8
|
+
module YAML
|
|
9
|
+
module Style
|
|
10
|
+
class KeyGrouping < Base
|
|
11
|
+
Finding = Struct.new(:key, :message, keyword_init: true)
|
|
12
|
+
ORDER_MSG = "Place `%<key>s` with the earlier `%<group>s` key group."
|
|
13
|
+
SPACE_MSG = "Separate the `%<group>s` key group with a blank line."
|
|
14
|
+
|
|
15
|
+
def on_new_investigation # rubocop:disable Metrics/AbcSize
|
|
16
|
+
return unless yaml_result.success?
|
|
17
|
+
|
|
18
|
+
findings(yaml_result.stream, settings).each do |finding|
|
|
19
|
+
key = finding.key
|
|
20
|
+
add_offense(source_range(key.location.line, key.location.column, key.value.length), message: finding.message)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def findings(root, options)
|
|
25
|
+
groups = options.fetch("Groups")
|
|
26
|
+
ranks = group_ranks(groups)
|
|
27
|
+
mappings(root).flat_map do |mapping, path|
|
|
28
|
+
next [] unless in_scope?(path, options.fetch("Scopes", ["*"]))
|
|
29
|
+
|
|
30
|
+
keys = mapping.children.each_slice(2).map(&:first)
|
|
31
|
+
keys = keys.select { |key| key.type == :scalar && key.value != "<<" }
|
|
32
|
+
ranked_findings(keys, groups, ranks, options)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def settings
|
|
39
|
+
cop_config.slice("Groups", "Scopes", "UnknownKeys", "RequireBlankLines")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def group_ranks(groups)
|
|
43
|
+
groups.each_with_index.with_object({}) do |(group, index), ranks|
|
|
44
|
+
group.fetch("Keys").each { |key| ranks[key] = [index, group.fetch("Name")] }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def mappings(node, path = []) # rubocop:disable Metrics/AbcSize
|
|
49
|
+
own = node.type == :mapping ? [[node, path]] : []
|
|
50
|
+
return own + node.children.flat_map { |child| mappings(child, path) } unless node.type == :mapping
|
|
51
|
+
|
|
52
|
+
children = node.children.each_slice(2).flat_map do |key, value|
|
|
53
|
+
key_path = key.type == :scalar ? path + [key.value] : path
|
|
54
|
+
mappings(value, key_path)
|
|
55
|
+
end
|
|
56
|
+
own + children
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def in_scope?(path, scopes)
|
|
60
|
+
scopes.include?("*") || scopes.include?(path.join("."))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def ranked_findings(keys, groups, ranks, options)
|
|
64
|
+
unknown_rank = options.fetch("UnknownKeys", "last") == "last" ? groups.length : nil
|
|
65
|
+
ranked = keys.filter_map do |key|
|
|
66
|
+
rank, name = ranks.fetch(key.value, [unknown_rank, "unlisted"])
|
|
67
|
+
[rank, name, key] if rank
|
|
68
|
+
end
|
|
69
|
+
order_findings(ranked) + spacing_findings(ranked, options.fetch("RequireBlankLines", false))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def order_findings(ranked)
|
|
73
|
+
ranked.each_cons(2).filter_map do |previous, current|
|
|
74
|
+
next unless current.first < previous.first
|
|
75
|
+
|
|
76
|
+
Finding.new(key: current.last, message: format(ORDER_MSG, key: current.last.value, group: current[1]))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def spacing_findings(ranked, required)
|
|
81
|
+
return [] unless required
|
|
82
|
+
|
|
83
|
+
ranked.each_cons(2).filter_map do |previous, current|
|
|
84
|
+
next if previous.first == current.first || current.last.location.line > previous.last.location.end_line + 1
|
|
85
|
+
|
|
86
|
+
Finding.new(key: current.last, message: format(SPACE_MSG, group: current[1]))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# lib/rubocop/cop/yaml/style/key_ordering.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../base"
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module Cop
|
|
8
|
+
module YAML
|
|
9
|
+
module Style
|
|
10
|
+
class KeyOrdering < Base
|
|
11
|
+
MSG = "Place `%<key>s` before `%<previous>s` according to the configured key order."
|
|
12
|
+
|
|
13
|
+
def on_new_investigation # rubocop:disable Metrics/AbcSize
|
|
14
|
+
return unless yaml_result.success?
|
|
15
|
+
|
|
16
|
+
violations(yaml_result.stream, style: style, keys: configured_keys).each do |key, previous|
|
|
17
|
+
add_offense(
|
|
18
|
+
source_range(key.location.line, key.location.column, key.value.length),
|
|
19
|
+
message: format(MSG, key: key.value, previous: previous.value)
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def violations(root, style:, keys: [])
|
|
25
|
+
mappings(root).flat_map do |mapping|
|
|
26
|
+
scalar_keys = mapping.children.each_slice(2).map(&:first)
|
|
27
|
+
next [] unless scalar_keys.all? { |key| sortable?(key) }
|
|
28
|
+
|
|
29
|
+
ordering_violations(scalar_keys, style, keys)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def mappings(node)
|
|
36
|
+
own = node.type == :mapping ? [node] : []
|
|
37
|
+
own + node.children.flat_map { |child| mappings(child) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ordering_violations(keys, style, configured)
|
|
41
|
+
ranked = ranked_keys(keys, style, configured)
|
|
42
|
+
ranked.each_cons(2).filter_map { |previous, key| [key.last, previous.last] if key.first < previous.first }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ranked_keys(keys, style, configured)
|
|
46
|
+
return keys.map { |key| [key.value.downcase, key] } if style.to_s == "alphabetical"
|
|
47
|
+
|
|
48
|
+
indexes = configured.each_with_index.to_h
|
|
49
|
+
keys.filter_map { |key| [indexes[key.value], key] if indexes.key?(key.value) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def sortable?(key)
|
|
53
|
+
key&.type == :scalar && key.value != "<<"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def style
|
|
57
|
+
cop_config.fetch("EnforcedStyle", "alphabetical")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def configured_keys
|
|
61
|
+
cop_config.fetch("Keys", [])
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# lib/rubocop/yaml/parser.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "psych"
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module Yaml
|
|
8
|
+
class Parser
|
|
9
|
+
Location = Struct.new(:line, :column, :end_line, :end_column, keyword_init: true)
|
|
10
|
+
Diagnostic = Struct.new(:path, :line, :column, :message, keyword_init: true)
|
|
11
|
+
Result = Struct.new(:stream, :diagnostic, keyword_init: true) do
|
|
12
|
+
def success?
|
|
13
|
+
diagnostic.nil?
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Node
|
|
18
|
+
attr_reader :type, :value, :children, :location, :anchor, :tag, :plain
|
|
19
|
+
|
|
20
|
+
def initialize(type:, value:, children:, location:, anchor:, tag:, plain:) # rubocop:disable Metrics/ParameterLists
|
|
21
|
+
@type = type
|
|
22
|
+
@value = value
|
|
23
|
+
@children = children.freeze
|
|
24
|
+
@location = location
|
|
25
|
+
@anchor = anchor
|
|
26
|
+
@tag = tag
|
|
27
|
+
@plain = plain
|
|
28
|
+
freeze
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def parse(source, path: "(yaml)")
|
|
33
|
+
Result.new(stream: convert(Psych.parse_stream(source, filename: path)))
|
|
34
|
+
rescue Psych::SyntaxError => e
|
|
35
|
+
Result.new(
|
|
36
|
+
diagnostic: Diagnostic.new(path: path, line: e.line, column: e.column, message: e.problem)
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def convert(node) # rubocop:disable Metrics/AbcSize
|
|
43
|
+
Node.new(
|
|
44
|
+
type: node.class.name.split("::").last.downcase.to_sym,
|
|
45
|
+
value: node.respond_to?(:value) ? node.value : nil,
|
|
46
|
+
children: Array(node.children).map { |child| convert(child) },
|
|
47
|
+
location: location(node),
|
|
48
|
+
anchor: node.respond_to?(:anchor) ? node.anchor : nil,
|
|
49
|
+
tag: node.respond_to?(:tag) ? node.tag : nil,
|
|
50
|
+
plain: node.respond_to?(:plain) ? node.plain : nil
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def location(node)
|
|
55
|
+
Location.new(
|
|
56
|
+
line: node.start_line + 1,
|
|
57
|
+
column: node.start_column + 1,
|
|
58
|
+
end_line: node.end_line + 1,
|
|
59
|
+
end_column: node.end_column + 1
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# lib/rubocop/yaml/plugin.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "lint_roller"
|
|
5
|
+
require "pathname"
|
|
6
|
+
|
|
7
|
+
require_relative "processed_source"
|
|
8
|
+
|
|
9
|
+
module RuboCop
|
|
10
|
+
module Yaml
|
|
11
|
+
class Plugin < LintRoller::Plugin
|
|
12
|
+
def about
|
|
13
|
+
LintRoller::About.new(
|
|
14
|
+
name: "rubocop-yaml",
|
|
15
|
+
version: VERSION,
|
|
16
|
+
homepage: "https://github.com/scarver2/rubocop-yaml",
|
|
17
|
+
description: "YAML analysis with native RuboCop offenses"
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def supported?(context)
|
|
22
|
+
context.engine == :rubocop
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def rules(_context)
|
|
26
|
+
LintRoller::Rules.new(
|
|
27
|
+
type: :path,
|
|
28
|
+
config_format: :rubocop,
|
|
29
|
+
value: Pathname(__dir__).join("../../../config/default.yml")
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# lib/rubocop/yaml/processed_source.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "rubocop"
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module Yaml
|
|
8
|
+
module ProcessedSource
|
|
9
|
+
YAML_EXTENSION = /\.ya?ml\z/i
|
|
10
|
+
|
|
11
|
+
def from_file(path, ruby_version, parser_engine: :default)
|
|
12
|
+
return super unless path.to_s.match?(YAML_EXTENSION)
|
|
13
|
+
|
|
14
|
+
yaml = File.binread(path)
|
|
15
|
+
source = new("", ruby_version, path, parser_engine: parser_engine)
|
|
16
|
+
buffer = ::Parser::Source::Buffer.new(path.to_s, source: yaml)
|
|
17
|
+
source.instance_variable_set(:@raw_source, yaml)
|
|
18
|
+
source.instance_variable_set(:@buffer, buffer)
|
|
19
|
+
source
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
RuboCop::ProcessedSource.singleton_class.prepend(RuboCop::Yaml::ProcessedSource)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# lib/rubocop/yaml/schema/registry.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module RuboCop
|
|
5
|
+
module Yaml
|
|
6
|
+
module Schema
|
|
7
|
+
class Registry
|
|
8
|
+
Entry = Struct.new(:name, :version, :path, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
def resolve(path)
|
|
11
|
+
case path
|
|
12
|
+
when %r{(?:\A|/)(?:compose|docker-compose)\.ya?ml\z}
|
|
13
|
+
entry("Docker Compose", "compose-spec-2026-08", "compose.json")
|
|
14
|
+
when %r{(?:\A|/)\.github/workflows/[^/]+\.ya?ml\z}
|
|
15
|
+
entry("GitHub Actions", "schema-store-2026-08", "github-actions.json")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def entry(name, version, filename)
|
|
22
|
+
Entry.new(
|
|
23
|
+
name: name,
|
|
24
|
+
version: version,
|
|
25
|
+
path: File.expand_path("../../../../config/schemas/#{filename}", __dir__)
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# lib/rubocop/yaml/schema/validator.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json_schemer"
|
|
5
|
+
require "pathname"
|
|
6
|
+
require "psych"
|
|
7
|
+
|
|
8
|
+
module RuboCop
|
|
9
|
+
module Yaml
|
|
10
|
+
module Schema
|
|
11
|
+
class Validator
|
|
12
|
+
Finding = Struct.new(:pointer, :message, :line, :column, keyword_init: true)
|
|
13
|
+
|
|
14
|
+
class ConfigurationError < StandardError; end
|
|
15
|
+
|
|
16
|
+
attr_reader :selected_schema
|
|
17
|
+
|
|
18
|
+
def initialize(parser: Parser.new, registry: Registry.new)
|
|
19
|
+
@parser = parser
|
|
20
|
+
@registry = registry
|
|
21
|
+
@schemas = {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def validate(source:, path:, schemas:, root: Dir.pwd, autodetect: true) # rubocop:disable Metrics/AbcSize
|
|
25
|
+
schema_path = schema_for(path, schemas, root, autodetect)
|
|
26
|
+
return [] unless schema_path
|
|
27
|
+
|
|
28
|
+
parsed = parser.parse(source, path: path)
|
|
29
|
+
return [] unless parsed.success?
|
|
30
|
+
|
|
31
|
+
data = to_data(parsed.stream.children.first.children.first)
|
|
32
|
+
schemer(schema_path).validate(data).map { |error| finding(error, parsed.stream) }
|
|
33
|
+
rescue JSON::ParserError, JSONSchemer::InvalidFileURI, JSONSchemer::UnknownRef, Psych::Exception => e
|
|
34
|
+
raise ConfigurationError, "#{schema_path}: #{e.message}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
attr_reader :parser, :registry, :schemas
|
|
40
|
+
|
|
41
|
+
def schema_for(path, mappings, root, autodetect)
|
|
42
|
+
relative = Pathname(path).absolute? ? Pathname(path).relative_path_from(Pathname(root)).to_s : path
|
|
43
|
+
match = mappings.find { |glob, _schema| File.fnmatch?(glob, relative, File::FNM_PATHNAME | File::FNM_EXTGLOB) }
|
|
44
|
+
return select_explicit(File.expand_path(match.last, root)) if match
|
|
45
|
+
return unless autodetect
|
|
46
|
+
|
|
47
|
+
select_registry(registry.resolve(relative))
|
|
48
|
+
rescue ArgumentError
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def select_explicit(path)
|
|
53
|
+
@selected_schema = Registry::Entry.new(name: "explicit", version: "project", path: path)
|
|
54
|
+
path
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def select_registry(entry)
|
|
58
|
+
@selected_schema = entry
|
|
59
|
+
entry&.path
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def schemer(path)
|
|
63
|
+
schemas[path] ||= begin
|
|
64
|
+
pathname = Pathname(path)
|
|
65
|
+
raise ConfigurationError, "schema does not exist" unless pathname.file?
|
|
66
|
+
raise ConfigurationError, "schema is malformed" unless JSONSchemer.valid_schema?(pathname)
|
|
67
|
+
|
|
68
|
+
JSONSchemer.schema(pathname)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def finding(error, stream)
|
|
73
|
+
node = node_at(stream, error.fetch("data_pointer"))
|
|
74
|
+
Finding.new(
|
|
75
|
+
pointer: error.fetch("data_pointer"),
|
|
76
|
+
message: error.fetch("error"),
|
|
77
|
+
line: node.location.line,
|
|
78
|
+
column: node.location.column
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def node_at(stream, pointer)
|
|
83
|
+
node = stream.children.first.children.first
|
|
84
|
+
decode_pointer(pointer).each do |part|
|
|
85
|
+
next_node = child_at(node, part)
|
|
86
|
+
break unless next_node
|
|
87
|
+
|
|
88
|
+
node = next_node
|
|
89
|
+
end
|
|
90
|
+
node
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def child_at(node, part)
|
|
94
|
+
return node.children[part.to_i] if node.type == :sequence
|
|
95
|
+
return unless node.type == :mapping
|
|
96
|
+
|
|
97
|
+
pair = node.children.each_slice(2).find { |key, _value| key.value == part }
|
|
98
|
+
pair&.last || pair&.first
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def decode_pointer(pointer)
|
|
102
|
+
pointer.split("/").drop(1).map { |part| part.gsub("~1", "/").gsub("~0", "~") }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def to_data(node)
|
|
106
|
+
case node.type
|
|
107
|
+
when :mapping
|
|
108
|
+
node.children.each_slice(2).to_h { |key, value| [key.value, to_data(value)] }
|
|
109
|
+
when :sequence
|
|
110
|
+
node.children.map { |child| to_data(child) }
|
|
111
|
+
when :scalar
|
|
112
|
+
scalar_value(node)
|
|
113
|
+
else
|
|
114
|
+
node.value
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def scalar_value(node) # rubocop:disable Metrics/AbcSize
|
|
119
|
+
return node.value unless node.plain
|
|
120
|
+
return nil if %w[null ~].include?(node.value.downcase)
|
|
121
|
+
return true if node.value.downcase == "true"
|
|
122
|
+
return false if node.value.downcase == "false"
|
|
123
|
+
return node.value.to_i if node.value.match?(/\A[-+]?\d+\z/)
|
|
124
|
+
return node.value.to_f if node.value.match?(/\A[-+]?(?:\d+\.\d*|\d*\.\d+)(?:e[-+]?\d+)?\z/i)
|
|
125
|
+
|
|
126
|
+
node.value
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
data/lib/rubocop/yaml.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# lib/rubocop/yaml.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "yaml/parser"
|
|
5
|
+
require_relative "yaml/processed_source"
|
|
6
|
+
# Plugin registration reads VERSION while the class is defined.
|
|
7
|
+
require_relative "yaml/version"
|
|
8
|
+
require_relative "yaml/plugin"
|
|
9
|
+
require_relative "yaml/schema/registry"
|
|
10
|
+
require_relative "yaml/schema/validator"
|
|
11
|
+
|
|
12
|
+
require_relative "cop/yaml/lint/duplicate_key"
|
|
13
|
+
require_relative "cop/yaml/lint/invalid_syntax"
|
|
14
|
+
require_relative "cop/yaml/rails/database_environment_consistency"
|
|
15
|
+
require_relative "cop/yaml/schema/validation"
|
|
16
|
+
require_relative "cop/yaml/style/key_grouping"
|
|
17
|
+
require_relative "cop/yaml/style/key_ordering"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# sig/rubocop/yaml/parser.rbs
|
|
2
|
+
module RuboCop
|
|
3
|
+
module Yaml
|
|
4
|
+
class Parser
|
|
5
|
+
class Location < Struct[Integer | nil]
|
|
6
|
+
def line: () -> Integer
|
|
7
|
+
def column: () -> Integer
|
|
8
|
+
def end_line: () -> Integer
|
|
9
|
+
def end_column: () -> Integer
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class Diagnostic < Struct[Integer | String | nil]
|
|
13
|
+
def path: () -> String
|
|
14
|
+
def line: () -> Integer
|
|
15
|
+
def column: () -> Integer
|
|
16
|
+
def message: () -> String
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class Result < Struct[Node | Diagnostic | nil]
|
|
20
|
+
def stream: () -> Node?
|
|
21
|
+
def diagnostic: () -> Diagnostic?
|
|
22
|
+
def success?: () -> bool
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class Node
|
|
26
|
+
attr_reader type: Symbol
|
|
27
|
+
attr_reader value: String?
|
|
28
|
+
attr_reader children: Array[Node]
|
|
29
|
+
attr_reader location: Location
|
|
30
|
+
attr_reader anchor: String?
|
|
31
|
+
attr_reader tag: String?
|
|
32
|
+
attr_reader plain: bool?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def parse: (String source, ?path: String) -> Result
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rubocop-yaml
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Stan Carver II
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: json_schemer
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.5'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '3.0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '2.5'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '3.0'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: lint_roller
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.1'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '1.1'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: rubocop
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '1.72'
|
|
53
|
+
- - "<"
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '2.0'
|
|
56
|
+
type: :runtime
|
|
57
|
+
prerelease: false
|
|
58
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '1.72'
|
|
63
|
+
- - "<"
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '2.0'
|
|
66
|
+
- !ruby/object:Gem::Dependency
|
|
67
|
+
name: rake
|
|
68
|
+
requirement: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - "~>"
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '13.2'
|
|
73
|
+
type: :development
|
|
74
|
+
prerelease: false
|
|
75
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - "~>"
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '13.2'
|
|
80
|
+
- !ruby/object:Gem::Dependency
|
|
81
|
+
name: rspec
|
|
82
|
+
requirement: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - "~>"
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: '3.13'
|
|
87
|
+
type: :development
|
|
88
|
+
prerelease: false
|
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - "~>"
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '3.13'
|
|
94
|
+
- !ruby/object:Gem::Dependency
|
|
95
|
+
name: simplecov
|
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - "~>"
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: 1.2.0
|
|
101
|
+
type: :development
|
|
102
|
+
prerelease: false
|
|
103
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - "~>"
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: 1.2.0
|
|
108
|
+
description: A RuboCop plugin for deterministic YAML linting, style, schema, and Rails
|
|
109
|
+
configuration checks.
|
|
110
|
+
email:
|
|
111
|
+
- howdy@stancarver.com
|
|
112
|
+
executables: []
|
|
113
|
+
extensions: []
|
|
114
|
+
extra_rdoc_files: []
|
|
115
|
+
files:
|
|
116
|
+
- CHANGELOG.md
|
|
117
|
+
- LICENSE
|
|
118
|
+
- README.md
|
|
119
|
+
- config/default.yml
|
|
120
|
+
- config/schemas/compose.json
|
|
121
|
+
- config/schemas/github-actions.json
|
|
122
|
+
- docs/README.md
|
|
123
|
+
- docs/releasing.md
|
|
124
|
+
- lib/rubocop/cop/yaml/base.rb
|
|
125
|
+
- lib/rubocop/cop/yaml/lint/duplicate_key.rb
|
|
126
|
+
- lib/rubocop/cop/yaml/lint/invalid_syntax.rb
|
|
127
|
+
- lib/rubocop/cop/yaml/rails/database_environment_consistency.rb
|
|
128
|
+
- lib/rubocop/cop/yaml/schema/validation.rb
|
|
129
|
+
- lib/rubocop/cop/yaml/style/key_grouping.rb
|
|
130
|
+
- lib/rubocop/cop/yaml/style/key_ordering.rb
|
|
131
|
+
- lib/rubocop/yaml.rb
|
|
132
|
+
- lib/rubocop/yaml/parser.rb
|
|
133
|
+
- lib/rubocop/yaml/plugin.rb
|
|
134
|
+
- lib/rubocop/yaml/processed_source.rb
|
|
135
|
+
- lib/rubocop/yaml/schema/registry.rb
|
|
136
|
+
- lib/rubocop/yaml/schema/validator.rb
|
|
137
|
+
- lib/rubocop/yaml/version.rb
|
|
138
|
+
- sig/rubocop/yaml/parser.rbs
|
|
139
|
+
homepage: https://github.com/scarver2/rubocop-yaml
|
|
140
|
+
licenses:
|
|
141
|
+
- MIT
|
|
142
|
+
metadata:
|
|
143
|
+
allowed_push_host: https://rubygems.org
|
|
144
|
+
default_lint_roller_plugin: RuboCop::Yaml::Plugin
|
|
145
|
+
source_code_uri: https://github.com/scarver2/rubocop-yaml
|
|
146
|
+
changelog_uri: https://github.com/scarver2/rubocop-yaml/blob/master/CHANGELOG.md
|
|
147
|
+
rubygems_mfa_required: 'true'
|
|
148
|
+
rdoc_options: []
|
|
149
|
+
require_paths:
|
|
150
|
+
- lib
|
|
151
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
152
|
+
requirements:
|
|
153
|
+
- - ">="
|
|
154
|
+
- !ruby/object:Gem::Version
|
|
155
|
+
version: '3.2'
|
|
156
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
157
|
+
requirements:
|
|
158
|
+
- - ">="
|
|
159
|
+
- !ruby/object:Gem::Version
|
|
160
|
+
version: '0'
|
|
161
|
+
requirements: []
|
|
162
|
+
rubygems_version: 4.0.16
|
|
163
|
+
specification_version: 4
|
|
164
|
+
summary: YAML analysis with native RuboCop offenses
|
|
165
|
+
test_files: []
|