yamlint 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +31 -9
  3. data/.serena/.gitignore +1 -0
  4. data/.serena/memories/project_overview.md +25 -0
  5. data/.serena/memories/style_and_conventions.md +7 -0
  6. data/.serena/memories/suggested_commands.md +17 -0
  7. data/.serena/memories/task_completion.md +5 -0
  8. data/.serena/project.yml +89 -0
  9. data/CHANGELOG.md +6 -2
  10. data/Gemfile +9 -7
  11. data/LICENSE.txt +1 -1
  12. data/README.md +155 -17
  13. data/Rakefile +10 -3
  14. data/docs/Yamlint/Cli.html +403 -0
  15. data/docs/Yamlint/Config.html +1034 -0
  16. data/docs/Yamlint/Formatter.html +433 -0
  17. data/docs/Yamlint/Linter.html +423 -0
  18. data/docs/Yamlint/Models/LintContext.html +993 -0
  19. data/docs/Yamlint/Models/Problem.html +951 -0
  20. data/docs/Yamlint/Models/Token.html +713 -0
  21. data/docs/Yamlint/Models.html +117 -0
  22. data/docs/Yamlint/Output/Base.html +290 -0
  23. data/docs/Yamlint/Output/Colored.html +293 -0
  24. data/docs/Yamlint/Output/Github.html +260 -0
  25. data/docs/Yamlint/Output/Parsable.html +258 -0
  26. data/docs/Yamlint/Output/Standard.html +270 -0
  27. data/docs/Yamlint/Output.html +208 -0
  28. data/docs/Yamlint/Parser/Comment.html +795 -0
  29. data/docs/Yamlint/Parser/CommentExtractor.html +287 -0
  30. data/docs/Yamlint/Parser/LineParser.html +423 -0
  31. data/docs/Yamlint/Parser/TokenParser/Handler.html +910 -0
  32. data/docs/Yamlint/Parser/TokenParser.html +291 -0
  33. data/docs/Yamlint/Parser.html +117 -0
  34. data/docs/Yamlint/Presets.html +266 -0
  35. data/docs/Yamlint/Rules/Anchors/AnchorHandler.html +678 -0
  36. data/docs/Yamlint/Rules/Anchors.html +314 -0
  37. data/docs/Yamlint/Rules/Base.html +968 -0
  38. data/docs/Yamlint/Rules/Braces.html +335 -0
  39. data/docs/Yamlint/Rules/Brackets.html +335 -0
  40. data/docs/Yamlint/Rules/Colons.html +313 -0
  41. data/docs/Yamlint/Rules/Commas.html +313 -0
  42. data/docs/Yamlint/Rules/CommentRule.html +298 -0
  43. data/docs/Yamlint/Rules/Comments.html +317 -0
  44. data/docs/Yamlint/Rules/CommentsIndentation.html +328 -0
  45. data/docs/Yamlint/Rules/DocumentEnd.html +332 -0
  46. data/docs/Yamlint/Rules/DocumentStart.html +332 -0
  47. data/docs/Yamlint/Rules/EmptyLines.html +300 -0
  48. data/docs/Yamlint/Rules/EmptyValues.html +273 -0
  49. data/docs/Yamlint/Rules/FloatValues.html +383 -0
  50. data/docs/Yamlint/Rules/Hyphens.html +337 -0
  51. data/docs/Yamlint/Rules/Indentation.html +329 -0
  52. data/docs/Yamlint/Rules/KeyDuplicates/DuplicateKeyHandler.html +716 -0
  53. data/docs/Yamlint/Rules/KeyDuplicates.html +258 -0
  54. data/docs/Yamlint/Rules/KeyOrdering/KeyOrderHandler.html +694 -0
  55. data/docs/Yamlint/Rules/KeyOrdering.html +258 -0
  56. data/docs/Yamlint/Rules/LineLength.html +251 -0
  57. data/docs/Yamlint/Rules/LineRule.html +304 -0
  58. data/docs/Yamlint/Rules/NewLineAtEndOfFile.html +308 -0
  59. data/docs/Yamlint/Rules/NewLines.html +310 -0
  60. data/docs/Yamlint/Rules/OctalValues.html +270 -0
  61. data/docs/Yamlint/Rules/QuotedStrings.html +381 -0
  62. data/docs/Yamlint/Rules/Registry.html +492 -0
  63. data/docs/Yamlint/Rules/TokenRule.html +298 -0
  64. data/docs/Yamlint/Rules/TrailingSpaces.html +321 -0
  65. data/docs/Yamlint/Rules/Truthy.html +288 -0
  66. data/docs/Yamlint/Rules.html +190 -0
  67. data/docs/Yamlint/Runner.html +551 -0
  68. data/docs/Yamlint.html +135 -0
  69. data/docs/_index.html +643 -0
  70. data/docs/class_list.html +54 -0
  71. data/docs/css/common.css +1 -0
  72. data/docs/css/full_list.css +58 -0
  73. data/docs/css/style.css +490 -0
  74. data/docs/file.README.html +276 -0
  75. data/docs/file_list.html +59 -0
  76. data/docs/frames.html +22 -0
  77. data/docs/index.html +276 -0
  78. data/docs/js/app.js +395 -0
  79. data/docs/js/full_list.js +244 -0
  80. data/docs/js/jquery.js +4 -0
  81. data/docs/method_list.html +1582 -0
  82. data/docs/top-level-namespace.html +110 -0
  83. data/exe/yamlint +7 -0
  84. data/lib/yamlint/cli.rb +162 -0
  85. data/lib/yamlint/config.rb +113 -0
  86. data/lib/yamlint/formatter.rb +140 -0
  87. data/lib/yamlint/linter.rb +130 -0
  88. data/lib/yamlint/models/lint_context.rb +43 -0
  89. data/lib/yamlint/models/problem.rb +44 -0
  90. data/lib/yamlint/models/token.rb +22 -0
  91. data/lib/yamlint/models.rb +9 -0
  92. data/lib/yamlint/output/base.rb +15 -0
  93. data/lib/yamlint/output/colored.rb +54 -0
  94. data/lib/yamlint/output/github.rb +20 -0
  95. data/lib/yamlint/output/parsable.rb +19 -0
  96. data/lib/yamlint/output/standard.rb +25 -0
  97. data/lib/yamlint/output.rb +26 -0
  98. data/lib/yamlint/parser/comment_extractor.rb +78 -0
  99. data/lib/yamlint/parser/line_parser.rb +30 -0
  100. data/lib/yamlint/parser/token_parser.rb +92 -0
  101. data/lib/yamlint/parser.rb +10 -0
  102. data/lib/yamlint/presets/default.rb +35 -0
  103. data/lib/yamlint/presets/relaxed.rb +34 -0
  104. data/lib/yamlint/presets.rb +19 -0
  105. data/lib/yamlint/rules/anchors.rb +133 -0
  106. data/lib/yamlint/rules/base.rb +102 -0
  107. data/lib/yamlint/rules/braces.rb +105 -0
  108. data/lib/yamlint/rules/brackets.rb +105 -0
  109. data/lib/yamlint/rules/colons.rb +73 -0
  110. data/lib/yamlint/rules/commas.rb +85 -0
  111. data/lib/yamlint/rules/comments.rb +77 -0
  112. data/lib/yamlint/rules/comments_indentation.rb +66 -0
  113. data/lib/yamlint/rules/document_end.rb +45 -0
  114. data/lib/yamlint/rules/document_start.rb +45 -0
  115. data/lib/yamlint/rules/empty_lines.rb +107 -0
  116. data/lib/yamlint/rules/empty_values.rb +43 -0
  117. data/lib/yamlint/rules/float_values.rb +67 -0
  118. data/lib/yamlint/rules/hyphens.rb +42 -0
  119. data/lib/yamlint/rules/indentation.rb +39 -0
  120. data/lib/yamlint/rules/key_duplicates.rb +127 -0
  121. data/lib/yamlint/rules/key_ordering.rb +126 -0
  122. data/lib/yamlint/rules/line_length.rb +57 -0
  123. data/lib/yamlint/rules/new_line_at_end_of_file.rb +31 -0
  124. data/lib/yamlint/rules/new_lines.rb +59 -0
  125. data/lib/yamlint/rules/octal_values.rb +62 -0
  126. data/lib/yamlint/rules/quoted_strings.rb +73 -0
  127. data/lib/yamlint/rules/registry.rb +48 -0
  128. data/lib/yamlint/rules/trailing_spaces.rb +31 -0
  129. data/lib/yamlint/rules/truthy.rb +48 -0
  130. data/lib/yamlint/rules.rb +41 -0
  131. data/lib/yamlint/runner.rb +80 -0
  132. data/lib/yamlint/version.rb +1 -1
  133. data/lib/yamlint.rb +11 -3
  134. metadata +131 -11
  135. data/CODE_OF_CONDUCT.md +0 -84
  136. data/sig/yamlint.rbs +0 -4
@@ -0,0 +1,110 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>
7
+ Top Level Namespace
8
+
9
+ &mdash; Documentation by YARD 0.9.38
10
+
11
+ </title>
12
+
13
+ <link rel="stylesheet" href="css/style.css" type="text/css" />
14
+
15
+ <link rel="stylesheet" href="css/common.css" type="text/css" />
16
+
17
+ <script type="text/javascript">
18
+ pathId = "";
19
+ relpath = '';
20
+ </script>
21
+
22
+
23
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
24
+
25
+ <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
26
+
27
+
28
+ </head>
29
+ <body>
30
+ <div class="nav_wrap">
31
+ <iframe id="nav" src="class_list.html?1"></iframe>
32
+ <div id="resizer"></div>
33
+ </div>
34
+
35
+ <div id="main" tabindex="-1">
36
+ <div id="header">
37
+ <div id="menu">
38
+
39
+ <a href="_index.html">Index</a> &raquo;
40
+
41
+
42
+ <span class="title">Top Level Namespace</span>
43
+
44
+ </div>
45
+
46
+ <div id="search">
47
+
48
+ <a class="full_list_link" id="class_list_link"
49
+ href="class_list.html">
50
+
51
+ <svg width="24" height="24">
52
+ <rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
53
+ <rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
54
+ <rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
55
+ </svg>
56
+ </a>
57
+
58
+ </div>
59
+ <div class="clear"></div>
60
+ </div>
61
+
62
+ <div id="content"><h1>Top Level Namespace
63
+
64
+
65
+
66
+ </h1>
67
+ <div class="box_info">
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+ </div>
80
+
81
+ <h2>Defined Under Namespace</h2>
82
+ <p class="children">
83
+
84
+
85
+ <strong class="modules">Modules:</strong> <span class='object_link'><a href="Yamlint.html" title="Yamlint (module)">Yamlint</a></span>
86
+
87
+
88
+
89
+
90
+ </p>
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+ </div>
101
+
102
+ <div id="footer">
103
+ Generated on Sun Jan 25 08:00:08 2026 by
104
+ <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
105
+ 0.9.38 (ruby-4.0.0).
106
+ </div>
107
+
108
+ </div>
109
+ </body>
110
+ </html>
data/exe/yamlint ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+ require 'yamlint'
6
+
7
+ exit Yamlint::Cli.new(ARGV).run
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module Yamlint
6
+ class Cli
7
+ attr_reader :options
8
+
9
+ def initialize(argv = ARGV)
10
+ @argv = argv
11
+ @options = {
12
+ config: nil,
13
+ format: 'colored',
14
+ fix: false,
15
+ dry_run: false,
16
+ strict: false
17
+ }
18
+ end
19
+
20
+ def run
21
+ parse_options
22
+ command = @argv.shift || 'lint'
23
+
24
+ case command
25
+ when 'lint', 'l'
26
+ run_lint
27
+ when 'format', 'fmt', 'fix'
28
+ run_format
29
+ when 'version', '-v', '--version'
30
+ puts "yamlint #{VERSION}"
31
+ 0
32
+ when 'help', '-h', '--help'
33
+ puts help_text
34
+ 0
35
+ else
36
+ if File.exist?(command) || File.directory?(command)
37
+ @argv.unshift(command)
38
+ run_lint
39
+ else
40
+ warn "Unknown command: #{command}"
41
+ warn help_text
42
+ 1
43
+ end
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def parse_options
50
+ parser = OptionParser.new do |opts|
51
+ opts.banner = 'Usage: yamlint [command] [options] [files...]'
52
+
53
+ opts.on('-c', '--config FILE', 'Configuration file') do |file|
54
+ @options[:config] = file
55
+ end
56
+
57
+ opts.on('-f', '--format FORMAT', 'Output format (standard, parsable, colored, github)') do |format|
58
+ @options[:format] = format
59
+ end
60
+
61
+ opts.on('--fix', 'Auto-fix problems') do
62
+ @options[:fix] = true
63
+ end
64
+
65
+ opts.on('--dry-run', 'Show what would be fixed without making changes') do
66
+ @options[:dry_run] = true
67
+ end
68
+
69
+ opts.on('-s', '--strict', 'Treat warnings as errors') do
70
+ @options[:strict] = true
71
+ end
72
+
73
+ opts.on('-v', '--version', 'Show version') do
74
+ puts "yamlint #{VERSION}"
75
+ exit 0
76
+ end
77
+
78
+ opts.on('-h', '--help', 'Show help') do
79
+ puts opts
80
+ exit 0
81
+ end
82
+ end
83
+
84
+ parser.order!(@argv)
85
+ end
86
+
87
+ def run_lint
88
+ paths = @argv.empty? ? ['.'] : @argv
89
+ config = load_config
90
+ runner = Runner.new(config:, output_format: @options[:format])
91
+
92
+ result = runner.lint(paths)
93
+
94
+ puts result[:output] unless result[:output].empty?
95
+ puts result[:summary] unless result[:summary].empty?
96
+
97
+ result[:exit_code]
98
+ end
99
+
100
+ def run_format
101
+ paths = @argv.empty? ? ['.'] : @argv
102
+ config = load_config
103
+ runner = Runner.new(config:, output_format: @options[:format])
104
+
105
+ result = runner.format(paths, dry_run: @options[:dry_run])
106
+
107
+ if @options[:dry_run]
108
+ if result[:changed].positive?
109
+ puts "Would fix #{result[:changed]} file(s):"
110
+ result[:changed_files].each { |f| puts " #{f}" }
111
+ else
112
+ puts 'No files would be changed'
113
+ end
114
+ elsif result[:changed].positive?
115
+ puts "Fixed #{result[:changed]} file(s)"
116
+ else
117
+ puts 'No files changed'
118
+ end
119
+
120
+ result[:exit_code]
121
+ end
122
+
123
+ def load_config
124
+ if @options[:config]
125
+ Config.load(@options[:config])
126
+ else
127
+ Config.load_default
128
+ end
129
+ end
130
+
131
+ def help_text
132
+ <<~HELP
133
+ yamlint - A YAML file linter and formatter
134
+
135
+ Usage:
136
+ yamlint [command] [options] [files...]
137
+
138
+ Commands:
139
+ lint, l Lint YAML files (default)
140
+ format, fmt Format YAML files
141
+ version Show version
142
+ help Show help
143
+
144
+ Options:
145
+ -c, --config FILE Configuration file
146
+ -f, --format FORMAT Output format (standard, parsable, colored, github)
147
+ --fix Auto-fix problems
148
+ --dry-run Show what would be fixed without making changes
149
+ -s, --strict Treat warnings as errors
150
+ -v, --version Show version
151
+ -h, --help Show help
152
+
153
+ Examples:
154
+ yamlint .
155
+ yamlint file.yaml
156
+ yamlint -c .yamllint.yml .
157
+ yamlint format --dry-run .
158
+ yamlint -f github .
159
+ HELP
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module Yamlint
6
+ class Config
7
+ DEFAULT_CONFIG_FILES = %w[
8
+ .yamllint
9
+ .yamllint.yaml
10
+ .yamllint.yml
11
+ ].freeze
12
+
13
+ YAML_FILE_PATTERNS = %w[
14
+ *.yaml
15
+ *.yml
16
+ ].freeze
17
+
18
+ attr_reader :rules, :yaml_files, :ignore, :extends
19
+
20
+ def initialize(options = {})
21
+ @extends = options[:extends]
22
+ @yaml_files = options[:'yaml-files'] || options['yaml-files'] || YAML_FILE_PATTERNS
23
+ @ignore = options[:ignore] || options['ignore'] || []
24
+ @rules = normalize_rules(options[:rules] || options['rules'] || {})
25
+ end
26
+
27
+ def self.load(path = nil)
28
+ if path
29
+ load_from_file(path)
30
+ else
31
+ load_default
32
+ end
33
+ end
34
+
35
+ def self.load_from_file(path)
36
+ content = File.read(path)
37
+ data = YAML.safe_load(content, permitted_classes: [Symbol]) || {}
38
+ build_from_hash(data)
39
+ end
40
+
41
+ def self.load_default
42
+ config_file = find_config_file
43
+ if config_file
44
+ load_from_file(config_file)
45
+ else
46
+ new_with_preset('default')
47
+ end
48
+ end
49
+
50
+ def self.find_config_file
51
+ DEFAULT_CONFIG_FILES.find { |f| File.exist?(f) }
52
+ end
53
+
54
+ def self.new_with_preset(preset_name)
55
+ preset = Presets.get(preset_name)
56
+ new(preset)
57
+ end
58
+
59
+ def self.build_from_hash(data)
60
+ base_config = if data['extends']
61
+ Presets.get(data['extends'])
62
+ else
63
+ {}
64
+ end
65
+
66
+ merged = deep_merge(base_config, symbolize_keys(data))
67
+ new(merged)
68
+ end
69
+
70
+ def rule_config(rule_id)
71
+ @rules[rule_id.to_s] || @rules[rule_id.to_sym] || {}
72
+ end
73
+
74
+ def rule_enabled?(rule_id)
75
+ config = rule_config(rule_id)
76
+ config != 'disable' && config != false
77
+ end
78
+
79
+ private
80
+
81
+ def normalize_rules(rules)
82
+ normalized = {}
83
+ rules.each do |key, value|
84
+ normalized[key.to_s] = value
85
+ end
86
+ normalized
87
+ end
88
+
89
+ class << self
90
+ private
91
+
92
+ def deep_merge(base, override)
93
+ result = base.dup
94
+ override.each do |key, value|
95
+ result[key] = if value.is_a?(Hash) && result[key].is_a?(Hash)
96
+ deep_merge(result[key], value)
97
+ else
98
+ value
99
+ end
100
+ end
101
+ result
102
+ end
103
+
104
+ def symbolize_keys(hash)
105
+ return hash unless hash.is_a?(Hash)
106
+
107
+ hash.transform_keys(&:to_sym).transform_values do |v|
108
+ v.is_a?(Hash) ? symbolize_keys(v) : v
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'yaml'
5
+
6
+ module Yamlint
7
+ class Formatter
8
+ attr_reader :config
9
+
10
+ def initialize(config = nil)
11
+ @config = config || Config.load_default
12
+ end
13
+
14
+ def format(content, dry_run: false)
15
+ original = content.dup
16
+ result = content
17
+
18
+ result = fix_trailing_spaces(result)
19
+ result = fix_new_line_at_end(result)
20
+ result = fix_new_lines(result)
21
+ result = fix_empty_lines(result)
22
+
23
+ return original if dry_run
24
+
25
+ if safe_fix?(original, result)
26
+ result
27
+ else
28
+ original
29
+ end
30
+ end
31
+
32
+ def format_file(filepath, dry_run: false)
33
+ content = File.read(filepath)
34
+ formatted = format(content, dry_run:)
35
+
36
+ File.write(filepath, formatted) unless dry_run || content == formatted
37
+
38
+ formatted
39
+ end
40
+
41
+ private
42
+
43
+ def fix_trailing_spaces(content)
44
+ content.gsub(/[ \t]+$/, '')
45
+ end
46
+
47
+ def fix_new_line_at_end(content)
48
+ return content if content.empty?
49
+ return content if content.end_with?("\n")
50
+
51
+ "#{content}\n"
52
+ end
53
+
54
+ def fix_new_lines(content)
55
+ new_line_type = @config.rules['new-lines']
56
+ return content unless new_line_type.is_a?(Hash)
57
+
58
+ type = new_line_type['type'] || new_line_type[:type] || 'unix'
59
+ case type
60
+ when 'unix'
61
+ content.gsub("\r\n", "\n")
62
+ when 'dos'
63
+ content.gsub(/(?<!\r)\n/, "\r\n")
64
+ else
65
+ content
66
+ end
67
+ end
68
+
69
+ def fix_empty_lines(content)
70
+ lines = content.lines
71
+ return content if lines.empty?
72
+
73
+ empty_lines_config = @config.rules['empty-lines'] || {}
74
+ return content unless empty_lines_config.is_a?(Hash)
75
+
76
+ max_start = empty_lines_config['max-start'] || empty_lines_config[:'max-start'] || 0
77
+ max_end = empty_lines_config['max-end'] || empty_lines_config[:'max-end'] || 0
78
+ max = empty_lines_config['max'] || empty_lines_config[:max] || 2
79
+
80
+ lines = remove_start_empty_lines(lines, max_start)
81
+ lines = remove_end_empty_lines(lines, max_end)
82
+ lines = limit_consecutive_empty_lines(lines, max)
83
+
84
+ lines.join
85
+ end
86
+
87
+ def remove_start_empty_lines(lines, max)
88
+ count = 0
89
+ lines.each do |line|
90
+ break unless line.strip.empty?
91
+
92
+ count += 1
93
+ end
94
+
95
+ lines.drop([count - max, 0].max)
96
+ end
97
+
98
+ def remove_end_empty_lines(lines, max)
99
+ count = 0
100
+ lines.reverse_each do |line|
101
+ break unless line.strip.empty?
102
+
103
+ count += 1
104
+ end
105
+
106
+ to_remove = [count - max, 0].max
107
+ to_remove.positive? ? lines[0...-to_remove] : lines
108
+ end
109
+
110
+ def limit_consecutive_empty_lines(lines, max)
111
+ result = []
112
+ consecutive_empty = 0
113
+
114
+ lines.each do |line|
115
+ if line.strip.empty?
116
+ consecutive_empty += 1
117
+ result << line if consecutive_empty <= max
118
+ else
119
+ consecutive_empty = 0
120
+ result << line
121
+ end
122
+ end
123
+
124
+ result
125
+ end
126
+
127
+ def safe_fix?(original, fixed)
128
+ return true if original == fixed
129
+ return false if original.empty? || fixed.empty?
130
+
131
+ begin
132
+ original_data = YAML.safe_load(original, permitted_classes: [Symbol, Date, Time])
133
+ fixed_data = YAML.safe_load(fixed, permitted_classes: [Symbol, Date, Time])
134
+ original_data == fixed_data
135
+ rescue Psych::SyntaxError
136
+ false
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require 'yaml'
5
+
6
+ module Yamlint
7
+ class Linter
8
+ attr_reader :config
9
+
10
+ def initialize(config = nil)
11
+ @config = config || Config.load_default
12
+ end
13
+
14
+ def lint(content, filepath: nil)
15
+ context = build_context(content, filepath)
16
+
17
+ syntax_problems = check_syntax(content, filepath)
18
+ return syntax_problems unless syntax_problems.empty?
19
+
20
+ parse_content(context)
21
+
22
+ rules = build_rules
23
+ rules.each do |rule|
24
+ problems = rule.check(context)
25
+ context.problems.concat(Array(problems))
26
+ end
27
+
28
+ filter_disabled_problems(context)
29
+ end
30
+
31
+ def lint_file(filepath)
32
+ content = File.read(filepath)
33
+ lint(content, filepath:)
34
+ end
35
+
36
+ private
37
+
38
+ def build_context(content, filepath)
39
+ Models::LintContext.new(
40
+ filepath: filepath || '<string>',
41
+ content:
42
+ )
43
+ end
44
+
45
+ def parse_content(context)
46
+ token_parser = Parser::TokenParser.new(context.content)
47
+ context.tokens.concat(token_parser.parse)
48
+
49
+ comment_extractor = Parser::CommentExtractor.new(context.content)
50
+ context.comments.concat(comment_extractor.extract)
51
+ end
52
+
53
+ def check_syntax(content, _filepath)
54
+ Psych.parse(content)
55
+ []
56
+ rescue Psych::SyntaxError => e
57
+ [Models::Problem.new(
58
+ line: e.line,
59
+ column: e.column,
60
+ rule: 'syntax',
61
+ level: :error,
62
+ message: e.message.sub(/^\([^)]+\): /, ''),
63
+ fixable: false
64
+ )]
65
+ end
66
+
67
+ def build_rules
68
+ Rules.load_all
69
+
70
+ enabled_rules = []
71
+ Rules::Registry.all.each do |rule_class|
72
+ rule_id = rule_class.id
73
+ next unless @config.rule_enabled?(rule_id)
74
+
75
+ rule_config = @config.rule_config(rule_id)
76
+ rule_options = rule_config.is_a?(Hash) ? normalize_options(rule_config) : {}
77
+ enabled_rules << rule_class.new(rule_options)
78
+ end
79
+
80
+ enabled_rules
81
+ end
82
+
83
+ def normalize_options(options)
84
+ normalized = {}
85
+ options.each do |key, value|
86
+ normalized[key.to_s.tr('_', '-').to_sym] = value
87
+ end
88
+ normalized
89
+ end
90
+
91
+ def filter_disabled_problems(context)
92
+ disabled_rules = Set.new
93
+ line_disabled = {}
94
+
95
+ context.comments.each do |comment|
96
+ if comment.disable_directive?
97
+ rules = comment.disabled_rules
98
+ if comment.text.include?('disable-line')
99
+ line_disabled[comment.line_number] ||= Set.new
100
+ if rules.empty?
101
+ line_disabled[comment.line_number] = :all
102
+ else
103
+ line_disabled[comment.line_number].merge(rules) unless line_disabled[comment.line_number] == :all
104
+ end
105
+ else
106
+ rules.empty? ? disabled_rules = :all : disabled_rules.merge(rules)
107
+ end
108
+ elsif comment.enable_directive?
109
+ rules = comment.disabled_rules
110
+ if rules.empty?
111
+ disabled_rules = Set.new
112
+ elsif disabled_rules.is_a?(Set)
113
+ disabled_rules.subtract(rules)
114
+ end
115
+ end
116
+ end
117
+
118
+ context.problems.reject do |problem|
119
+ next true if disabled_rules == :all
120
+ next true if disabled_rules.include?(problem.rule)
121
+
122
+ line_disable = line_disabled[problem.line]
123
+ next true if line_disable == :all
124
+ next true if line_disable&.include?(problem.rule)
125
+
126
+ false
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yamlint
4
+ module Models
5
+ class LintContext
6
+ attr_reader :filepath, :content, :lines, :tokens, :comments
7
+ attr_accessor :problems
8
+
9
+ def initialize(filepath:, content:)
10
+ @filepath = filepath
11
+ @content = content
12
+ @lines = content.lines
13
+ @tokens = []
14
+ @comments = []
15
+ @problems = []
16
+ end
17
+
18
+ def add_problem(problem)
19
+ @problems << problem
20
+ end
21
+
22
+ def add_token(token)
23
+ @tokens << token
24
+ end
25
+
26
+ def add_comment(comment)
27
+ @comments << comment
28
+ end
29
+
30
+ def line_count
31
+ @lines.length
32
+ end
33
+
34
+ def line_at(line_number)
35
+ @lines[line_number - 1]
36
+ end
37
+
38
+ def empty?
39
+ @content.empty?
40
+ end
41
+ end
42
+ end
43
+ end