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 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
@@ -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
@@ -0,0 +1,8 @@
1
+ # lib/rubocop/yaml/version.rb
2
+ # frozen_string_literal: true
3
+
4
+ module RuboCop
5
+ module Yaml
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
@@ -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: []