yard-lint 1.2.3 → 1.3.0.rc1

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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +150 -1
  3. data/README.md +98 -4
  4. data/Rakefile +20 -0
  5. data/bin/yard-lint +71 -38
  6. data/lib/yard/lint/config.rb +5 -0
  7. data/lib/yard/lint/config_updater.rb +222 -0
  8. data/lib/yard/lint/errors.rb +6 -0
  9. data/lib/yard/lint/executor/in_process_registry.rb +130 -0
  10. data/lib/yard/lint/executor/query_executor.rb +109 -0
  11. data/lib/yard/lint/executor/result_collector.rb +55 -0
  12. data/lib/yard/lint/executor/warning_dispatcher.rb +79 -0
  13. data/lib/yard/lint/results/base.rb +2 -1
  14. data/lib/yard/lint/runner.rb +50 -38
  15. data/lib/yard/lint/templates/default_config.yml +105 -0
  16. data/lib/yard/lint/templates/strict_config.yml +105 -0
  17. data/lib/yard/lint/validators/base.rb +52 -118
  18. data/lib/yard/lint/validators/documentation/blank_line_before_definition/config.rb +25 -0
  19. data/lib/yard/lint/validators/documentation/blank_line_before_definition/messages_builder.rb +39 -0
  20. data/lib/yard/lint/validators/documentation/blank_line_before_definition/parser.rb +59 -0
  21. data/lib/yard/lint/validators/documentation/blank_line_before_definition/result.rb +61 -0
  22. data/lib/yard/lint/validators/documentation/blank_line_before_definition/validator.rb +94 -0
  23. data/lib/yard/lint/validators/documentation/blank_line_before_definition.rb +63 -0
  24. data/lib/yard/lint/validators/documentation/empty_comment_line/config.rb +24 -0
  25. data/lib/yard/lint/validators/documentation/empty_comment_line/messages_builder.rb +34 -0
  26. data/lib/yard/lint/validators/documentation/empty_comment_line/parser.rb +60 -0
  27. data/lib/yard/lint/validators/documentation/empty_comment_line/result.rb +25 -0
  28. data/lib/yard/lint/validators/documentation/empty_comment_line/validator.rb +109 -0
  29. data/lib/yard/lint/validators/documentation/empty_comment_line.rb +58 -0
  30. data/lib/yard/lint/validators/documentation/markdown_syntax/validator.rb +36 -21
  31. data/lib/yard/lint/validators/documentation/markdown_syntax.rb +0 -1
  32. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/validator.rb +19 -29
  33. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods.rb +0 -1
  34. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/validator.rb +18 -34
  35. data/lib/yard/lint/validators/documentation/undocumented_method_arguments.rb +0 -1
  36. data/lib/yard/lint/validators/documentation/undocumented_objects/validator.rb +17 -25
  37. data/lib/yard/lint/validators/documentation/undocumented_objects.rb +4 -5
  38. data/lib/yard/lint/validators/documentation/undocumented_options/validator.rb +30 -21
  39. data/lib/yard/lint/validators/documentation/undocumented_options.rb +0 -1
  40. data/lib/yard/lint/validators/semantic/abstract_methods/result.rb +2 -2
  41. data/lib/yard/lint/validators/semantic/abstract_methods/validator.rb +31 -43
  42. data/lib/yard/lint/validators/semantic/abstract_methods.rb +0 -1
  43. data/lib/yard/lint/validators/tags/api_tags/validator.rb +24 -39
  44. data/lib/yard/lint/validators/tags/api_tags.rb +0 -1
  45. data/lib/yard/lint/validators/tags/collection_type/validator.rb +37 -66
  46. data/lib/yard/lint/validators/tags/collection_type.rb +0 -1
  47. data/lib/yard/lint/validators/tags/example_syntax/validator.rb +51 -64
  48. data/lib/yard/lint/validators/tags/example_syntax.rb +0 -1
  49. data/lib/yard/lint/validators/tags/informal_notation/config.rb +40 -0
  50. data/lib/yard/lint/validators/tags/informal_notation/messages_builder.rb +35 -0
  51. data/lib/yard/lint/validators/tags/informal_notation/parser.rb +55 -0
  52. data/lib/yard/lint/validators/tags/informal_notation/result.rb +26 -0
  53. data/lib/yard/lint/validators/tags/informal_notation/validator.rb +133 -0
  54. data/lib/yard/lint/validators/tags/informal_notation.rb +45 -0
  55. data/lib/yard/lint/validators/tags/invalid_types/validator.rb +57 -70
  56. data/lib/yard/lint/validators/tags/invalid_types.rb +0 -1
  57. data/lib/yard/lint/validators/tags/meaningless_tag/validator.rb +22 -54
  58. data/lib/yard/lint/validators/tags/meaningless_tag.rb +0 -1
  59. data/lib/yard/lint/validators/tags/non_ascii_type/config.rb +21 -0
  60. data/lib/yard/lint/validators/tags/non_ascii_type/messages_builder.rb +29 -0
  61. data/lib/yard/lint/validators/tags/non_ascii_type/parser.rb +59 -0
  62. data/lib/yard/lint/validators/tags/non_ascii_type/result.rb +25 -0
  63. data/lib/yard/lint/validators/tags/non_ascii_type/validator.rb +50 -0
  64. data/lib/yard/lint/validators/tags/non_ascii_type.rb +39 -0
  65. data/lib/yard/lint/validators/tags/option_tags/result.rb +2 -2
  66. data/lib/yard/lint/validators/tags/option_tags/validator.rb +25 -40
  67. data/lib/yard/lint/validators/tags/option_tags.rb +0 -1
  68. data/lib/yard/lint/validators/tags/order/validator.rb +28 -55
  69. data/lib/yard/lint/validators/tags/order.rb +0 -1
  70. data/lib/yard/lint/validators/tags/redundant_param_description/config.rb +15 -1
  71. data/lib/yard/lint/validators/tags/redundant_param_description/messages_builder.rb +5 -0
  72. data/lib/yard/lint/validators/tags/redundant_param_description/validator.rb +134 -100
  73. data/lib/yard/lint/validators/tags/redundant_param_description.rb +0 -1
  74. data/lib/yard/lint/validators/tags/tag_group_separator/config.rb +29 -0
  75. data/lib/yard/lint/validators/tags/tag_group_separator/messages_builder.rb +49 -0
  76. data/lib/yard/lint/validators/tags/tag_group_separator/parser.rb +67 -0
  77. data/lib/yard/lint/validators/tags/tag_group_separator/result.rb +28 -0
  78. data/lib/yard/lint/validators/tags/tag_group_separator/validator.rb +117 -0
  79. data/lib/yard/lint/validators/tags/tag_group_separator.rb +49 -0
  80. data/lib/yard/lint/validators/tags/tag_type_position/validator.rb +53 -84
  81. data/lib/yard/lint/validators/tags/tag_type_position.rb +0 -1
  82. data/lib/yard/lint/validators/tags/type_syntax/parser.rb +7 -2
  83. data/lib/yard/lint/validators/tags/type_syntax/validator.rb +29 -59
  84. data/lib/yard/lint/validators/tags/type_syntax.rb +0 -1
  85. data/lib/yard/lint/validators/warnings/duplicated_parameter_name/validator.rb +1 -18
  86. data/lib/yard/lint/validators/warnings/invalid_directive_format/validator.rb +1 -18
  87. data/lib/yard/lint/validators/warnings/invalid_tag_format/validator.rb +1 -18
  88. data/lib/yard/lint/validators/warnings/unknown_directive/validator.rb +1 -18
  89. data/lib/yard/lint/validators/warnings/unknown_parameter_name/messages_builder.rb +243 -0
  90. data/lib/yard/lint/validators/warnings/unknown_parameter_name/result.rb +4 -3
  91. data/lib/yard/lint/validators/warnings/unknown_parameter_name/validator.rb +1 -18
  92. data/lib/yard/lint/validators/warnings/unknown_tag/messages_builder.rb +144 -0
  93. data/lib/yard/lint/validators/warnings/unknown_tag/result.rb +4 -3
  94. data/lib/yard/lint/validators/warnings/unknown_tag/validator.rb +1 -18
  95. data/lib/yard/lint/validators/warnings/unknown_tag.rb +10 -0
  96. data/lib/yard/lint/version.rb +1 -1
  97. data/lib/yard/lint.rb +81 -13
  98. data/renovate.json +1 -8
  99. metadata +38 -2
  100. data/lib/yard/lint/command_cache.rb +0 -93
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Documentation
7
+ module BlankLineBeforeDefinition
8
+ # Parses YARD output for blank line before definition violations
9
+ class Parser < Parsers::Base
10
+ # Parse YARD output into structured violations
11
+ # @param output [String] raw YARD output
12
+ # @return [Array<Hash>] array of violation hashes
13
+ def call(output)
14
+ return [] if output.nil? || output.empty?
15
+
16
+ violations = []
17
+ lines = output.lines.map(&:chomp)
18
+
19
+ i = 0
20
+ while i < lines.size
21
+ line = lines[i]
22
+
23
+ # Match location line: "file:line: object_name"
24
+ if (location_match = line.match(/^(.+):(\d+): (.+)$/))
25
+ file_path = location_match[1]
26
+ object_line = location_match[2].to_i
27
+ object_name = location_match[3]
28
+
29
+ # Next line contains violation details
30
+ i += 1
31
+ next unless i < lines.size
32
+
33
+ # Parse violation: "single:1" or "orphaned:3"
34
+ detail_parts = lines[i].split(':', 2)
35
+ next unless detail_parts.size == 2
36
+
37
+ violation_type = detail_parts[0]
38
+ blank_count = detail_parts[1].to_i
39
+
40
+ violations << {
41
+ location: file_path,
42
+ line: object_line,
43
+ object_name: object_name,
44
+ violation_type: violation_type,
45
+ blank_count: blank_count
46
+ }
47
+ end
48
+
49
+ i += 1
50
+ end
51
+
52
+ violations
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Documentation
7
+ module BlankLineBeforeDefinition
8
+ # Result builder for blank line before definition violations
9
+ class Result < Results::Base
10
+ self.default_severity = 'convention'
11
+ self.offense_type = 'line'
12
+ self.offense_name = 'BlankLineBeforeDefinition'
13
+
14
+ # Build human-readable message for blank line violation
15
+ # @param offense [Hash] offense data with violation details
16
+ # @return [String] formatted message
17
+ def build_message(offense)
18
+ MessagesBuilder.call(offense)
19
+ end
20
+
21
+ private
22
+
23
+ # Override to handle per-violation severity based on violation type
24
+ # @return [Array<Hash>] array of offense hashes
25
+ def build_offenses
26
+ @parsed_data.map do |offense_data|
27
+ severity = severity_for_violation(offense_data[:violation_type])
28
+
29
+ offense_data.merge(
30
+ severity: severity,
31
+ type: self.class.offense_type,
32
+ name: computed_offense_name,
33
+ message: build_message(offense_data),
34
+ location: offense_data[:location] || offense_data[:file],
35
+ location_line: offense_data[:line] || offense_data[:location_line] || 0
36
+ )
37
+ end
38
+ end
39
+
40
+ # Get severity for a specific violation type
41
+ # @param violation_type [String] 'single' or 'orphaned'
42
+ # @return [String] severity level
43
+ def severity_for_violation(violation_type)
44
+ default = self.class.default_severity
45
+ return default unless config
46
+
47
+ case violation_type
48
+ when 'orphaned'
49
+ config.validator_config(validator_name, 'OrphanedSeverity') ||
50
+ config.validator_severity(validator_name) ||
51
+ default
52
+ else
53
+ config.validator_severity(validator_name) || default
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Documentation
7
+ module BlankLineBeforeDefinition
8
+ # Validates blank lines between documentation and definitions
9
+ class Validator < Validators::Base
10
+ # Enable in-process execution for this validator
11
+ in_process visibility: :public
12
+
13
+ # Execute query for a single object during in-process execution.
14
+ # Checks for blank lines between documentation blocks and definitions.
15
+ # @param object [YARD::CodeObjects::Base] the code object to query
16
+ # @param collector [Executor::ResultCollector] collector for output
17
+ # @return [void]
18
+ def in_process_query(object, collector)
19
+ return unless object.file && File.exist?(object.file) && object.line.to_i > 1
20
+
21
+ source_lines = File.readlines(object.file)
22
+ definition_line = object.line - 1
23
+
24
+ blank_count, has_doc_block = analyze_spacing(source_lines, definition_line)
25
+
26
+ return if blank_count.zero? || !has_doc_block
27
+
28
+ violation_type = blank_count >= 2 ? 'orphaned' : 'single'
29
+
30
+ return unless pattern_enabled?(violation_type)
31
+
32
+ collector.puts "#{object.file}:#{object.line}: #{object.title}"
33
+ collector.puts "#{violation_type}:#{blank_count}"
34
+ end
35
+
36
+ private
37
+
38
+ # Analyze spacing between documentation and definition
39
+ # @param source_lines [Array<String>] lines of source file
40
+ # @param definition_line [Integer] 0-indexed line of definition
41
+ # @return [Array<Integer, Boolean>] blank count and whether doc block exists
42
+ def analyze_spacing(source_lines, definition_line)
43
+ blank_count = 0
44
+ has_doc_block = false
45
+
46
+ (definition_line - 1).downto(0) do |i|
47
+ line = source_lines[i].to_s.rstrip
48
+ stripped = line.strip
49
+
50
+ if stripped.empty?
51
+ blank_count += 1
52
+ elsif stripped.start_with?('#')
53
+ # Skip Ruby magic comments - they're not YARD documentation
54
+ next if magic_comment?(stripped)
55
+
56
+ has_doc_block = true
57
+ break
58
+ else
59
+ # Non-comment, non-blank line - no documentation above
60
+ break
61
+ end
62
+ end
63
+
64
+ [blank_count, has_doc_block]
65
+ end
66
+
67
+ # Check if a comment line is a Ruby magic comment
68
+ # @param line [String] stripped comment line
69
+ # @return [Boolean] true if line is a magic comment
70
+ def magic_comment?(line)
71
+ # Ruby magic comments: frozen_string_literal, encoding, warn_indent, shareable_constant_value
72
+ line.match?(/^#\s*(frozen[_-]string[_-]literal|encoding|warn[_-]indent|shareable[_-]constant[_-]value)\s*:/i)
73
+ end
74
+
75
+ # Check if the given pattern is enabled in configuration
76
+ # @param violation_type [String] 'single' or 'orphaned'
77
+ # @return [Boolean] whether the pattern is enabled
78
+ def pattern_enabled?(violation_type)
79
+ patterns = config_or_default('EnabledPatterns')
80
+ case violation_type
81
+ when 'single'
82
+ patterns['SingleBlankLine']
83
+ when 'orphaned'
84
+ patterns['OrphanedDocs']
85
+ else
86
+ true
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Documentation
7
+ # BlankLineBeforeDefinition validator
8
+ #
9
+ # Detects blank lines between YARD documentation and method/class/module definitions.
10
+ # YARD requires documentation to be immediately adjacent to the definition it documents.
11
+ #
12
+ # @example Bad - Single blank line (convention violation)
13
+ # # Description of the method
14
+ # # @param value [String] the value
15
+ #
16
+ # def process(value)
17
+ # end
18
+ #
19
+ # @example Bad - Multiple blank lines (orphaned documentation)
20
+ # # Description of the method
21
+ # # @param value [String] the value
22
+ #
23
+ #
24
+ # def process(value)
25
+ # end
26
+ #
27
+ # @example Good - No blank lines
28
+ # # Description of the method
29
+ # # @param value [String] the value
30
+ # def process(value)
31
+ # end
32
+ #
33
+ # ## Severity Levels
34
+ #
35
+ # - **1 blank line**: Convention violation - YARD associates the doc but this
36
+ # violates formatting conventions
37
+ # - **2+ blank lines**: Orphaned documentation - YARD ignores the documentation entirely
38
+ #
39
+ # ## Configuration
40
+ #
41
+ # To customize severity levels:
42
+ #
43
+ # Documentation/BlankLineBeforeDefinition:
44
+ # Severity: warning # For single blank line
45
+ # OrphanedSeverity: error # For 2+ blank lines
46
+ #
47
+ # To check only single blank lines:
48
+ #
49
+ # Documentation/BlankLineBeforeDefinition:
50
+ # EnabledPatterns:
51
+ # SingleBlankLine: true
52
+ # OrphanedDocs: false
53
+ #
54
+ # To disable this validator:
55
+ #
56
+ # Documentation/BlankLineBeforeDefinition:
57
+ # Enabled: false
58
+ module BlankLineBeforeDefinition
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Documentation
7
+ module EmptyCommentLine
8
+ # Configuration for EmptyCommentLine validator
9
+ class Config < ::Yard::Lint::Validators::Config
10
+ self.id = :empty_comment_line
11
+ self.defaults = {
12
+ 'Enabled' => true,
13
+ 'Severity' => 'convention',
14
+ 'EnabledPatterns' => {
15
+ 'Leading' => true,
16
+ 'Trailing' => true
17
+ }
18
+ }.freeze
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Documentation
7
+ module EmptyCommentLine
8
+ # Builds human-readable messages for empty comment line violations
9
+ class MessagesBuilder
10
+ # Maps violation types to human-readable descriptions
11
+ ERROR_DESCRIPTIONS = {
12
+ 'leading' => 'Empty leading comment line in documentation',
13
+ 'trailing' => 'Empty trailing comment line in documentation'
14
+ }.freeze
15
+
16
+ class << self
17
+ # Formats a violation message
18
+ # @param offense [Hash] the offense details
19
+ # @return [String] formatted message
20
+ def call(offense)
21
+ type = offense[:violation_type]
22
+ object_name = offense[:object_name]
23
+
24
+ description = ERROR_DESCRIPTIONS[type] || 'Empty comment line in documentation'
25
+
26
+ "#{description} for '#{object_name}'"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Documentation
7
+ module EmptyCommentLine
8
+ # Parses YARD output for empty comment line violations
9
+ class Parser < Parsers::Base
10
+ # Parse YARD output into structured violations
11
+ # @param output [String] raw YARD output
12
+ # @return [Array<Hash>] array of violation hashes
13
+ def call(output)
14
+ return [] if output.nil? || output.empty?
15
+
16
+ violations = []
17
+ lines = output.lines.map(&:chomp)
18
+
19
+ i = 0
20
+ while i < lines.size
21
+ line = lines[i]
22
+
23
+ # Match location line: "file:line: object_name"
24
+ if (location_match = line.match(/^(.+):(\d+): (.+)$/))
25
+ file_path = location_match[1]
26
+ object_line = location_match[2].to_i
27
+ object_name = location_match[3]
28
+
29
+ # Next line contains violation details
30
+ i += 1
31
+ next unless i < lines.size
32
+
33
+ # Parse violations: "leading:5|trailing:10"
34
+ violation_parts = lines[i].split('|')
35
+
36
+ violation_parts.each do |part|
37
+ type, line_num = part.split(':', 2)
38
+ next unless type && line_num
39
+
40
+ violations << {
41
+ location: file_path,
42
+ line: line_num.to_i,
43
+ object_line: object_line,
44
+ object_name: object_name,
45
+ violation_type: type
46
+ }
47
+ end
48
+ end
49
+
50
+ i += 1
51
+ end
52
+
53
+ violations
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Documentation
7
+ module EmptyCommentLine
8
+ # Result builder for empty comment line violations
9
+ class Result < Results::Base
10
+ self.default_severity = 'convention'
11
+ self.offense_type = 'line'
12
+ self.offense_name = 'EmptyCommentLine'
13
+
14
+ # Build human-readable message for empty comment line offense
15
+ # @param offense [Hash] offense data with violation details
16
+ # @return [String] formatted message
17
+ def build_message(offense)
18
+ MessagesBuilder.call(offense)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Documentation
7
+ module EmptyCommentLine
8
+ # Validates empty comment lines at the start/end of documentation blocks
9
+ class Validator < Validators::Base
10
+ # Enable in-process execution for this validator
11
+ in_process visibility: :public
12
+
13
+ # Execute query for a single object during in-process execution.
14
+ # Checks for empty leading/trailing comment lines in documentation blocks.
15
+ # @param object [YARD::CodeObjects::Base] the code object to query
16
+ # @param collector [Executor::ResultCollector] collector for output
17
+ # @return [void]
18
+ def in_process_query(object, collector)
19
+ return unless object.file && File.exist?(object.file) && object.line.to_i > 1
20
+
21
+ check_leading = check_leading?
22
+ check_trailing = check_trailing?
23
+
24
+ source_lines = File.readlines(object.file)
25
+ definition_line = object.line - 1
26
+
27
+ # Find comment block boundaries
28
+ comment_end = nil
29
+ comment_start = nil
30
+
31
+ (definition_line - 1).downto(0) do |i|
32
+ line = source_lines[i].to_s.rstrip
33
+ stripped = line.strip
34
+
35
+ if stripped.empty? && comment_end.nil?
36
+ # Skip empty lines before finding comment block
37
+ next
38
+ elsif stripped.start_with?('#')
39
+ comment_end ||= i
40
+ comment_start = i
41
+ else
42
+ break
43
+ end
44
+ end
45
+
46
+ return unless comment_start && comment_end
47
+
48
+ comment_block = source_lines[comment_start..comment_end]
49
+
50
+ # Find first and last content lines
51
+ first_content_idx = nil
52
+ last_content_idx = nil
53
+
54
+ comment_block.each_with_index do |line, idx|
55
+ stripped = line.strip
56
+ has_content = stripped.match?(/^#.+\S/)
57
+ if has_content
58
+ first_content_idx ||= idx
59
+ last_content_idx = idx
60
+ end
61
+ end
62
+
63
+ return unless first_content_idx && last_content_idx
64
+
65
+ violations = []
66
+
67
+ # Check for leading empty comment lines
68
+ if check_leading
69
+ (0...first_content_idx).each do |i|
70
+ if comment_block[i].strip.match?(/^#\s*$/)
71
+ violations << "leading:#{comment_start + i + 1}"
72
+ end
73
+ end
74
+ end
75
+
76
+ # Check for trailing empty comment lines
77
+ if check_trailing
78
+ ((last_content_idx + 1)...comment_block.length).each do |i|
79
+ if comment_block[i].strip.match?(/^#\s*$/)
80
+ violations << "trailing:#{comment_start + i + 1}"
81
+ end
82
+ end
83
+ end
84
+
85
+ return if violations.empty?
86
+
87
+ collector.puts "#{object.file}:#{object.line}: #{object.title}"
88
+ collector.puts violations.join('|')
89
+ end
90
+
91
+ private
92
+
93
+ # @return [Boolean] whether to check for leading empty lines
94
+ def check_leading?
95
+ patterns = config_or_default('EnabledPatterns')
96
+ patterns['Leading']
97
+ end
98
+
99
+ # @return [Boolean] whether to check for trailing empty lines
100
+ def check_trailing?
101
+ patterns = config_or_default('EnabledPatterns')
102
+ patterns['Trailing']
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Documentation
7
+ # EmptyCommentLine validator
8
+ #
9
+ # Detects empty comment lines at the start or end of YARD documentation blocks.
10
+ # Empty lines BETWEEN tag groups are allowed for readability.
11
+ #
12
+ # @example Bad - Empty leading comment line
13
+ # #
14
+ # # Description of the method
15
+ # # @param value [String] the value
16
+ # def process(value)
17
+ # end
18
+ #
19
+ # @example Bad - Empty trailing comment line
20
+ # # Description of the method
21
+ # # @param value [String] the value
22
+ # #
23
+ # def process(value)
24
+ # end
25
+ #
26
+ # @example Good - No leading or trailing empty lines
27
+ # # Description of the method
28
+ # # @param value [String] the value
29
+ # def process(value)
30
+ # end
31
+ #
32
+ # @example Good - Empty line between sections (allowed)
33
+ # # Description of the method
34
+ # #
35
+ # # @param value [String] the value
36
+ # # @return [Boolean] success
37
+ # def process(value)
38
+ # end
39
+ #
40
+ # ## Configuration
41
+ #
42
+ # To check only leading empty lines:
43
+ #
44
+ # Documentation/EmptyCommentLine:
45
+ # EnabledPatterns:
46
+ # Leading: true
47
+ # Trailing: false
48
+ #
49
+ # To disable this validator:
50
+ #
51
+ # Documentation/EmptyCommentLine:
52
+ # Enabled: false
53
+ module EmptyCommentLine
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -7,28 +7,43 @@ module Yard
7
7
  module MarkdownSyntax
8
8
  # Validates markdown syntax in documentation
9
9
  class Validator < Validators::Base
10
- # YARD query to extract docstrings and check for markdown errors
11
- # @return [String] YARD Ruby query code
12
- def query
13
- <<~QUERY.strip
14
- 'docstring_text = object.docstring.to_s; unless docstring_text.empty?; errors = []; backtick_count = docstring_text.scan(/\\x60/).count; errors << "unclosed_backtick" if backtick_count.odd?; code_block_count = docstring_text.scan(/^```/).count; errors << "unclosed_code_block" if code_block_count.odd?; non_code_text = docstring_text.gsub(/\\x60[^\\x60]*\\x60/, ""); bold_count = non_code_text.scan(/\\*\\*/).count; errors << "unclosed_bold" if bold_count.odd?; lines = docstring_text.lines; lines.each_with_index do |line, line_idx|; stripped = line.strip; errors << "invalid_list_marker:" + (line_idx + 1).to_s if stripped =~ /^[•·]/; end; unless errors.empty?; puts object.file + ":" + object.line.to_s + ": " + object.title; puts errors.join("|"); end; end; false'
15
- QUERY
16
- end
10
+ # Enable in-process execution for this validator
11
+ in_process visibility: :public
12
+
13
+ # Execute query for a single object during in-process execution.
14
+ # Checks for markdown syntax errors in docstrings.
15
+ # @param object [YARD::CodeObjects::Base] the code object to query
16
+ # @param collector [Executor::ResultCollector] collector for output
17
+ # @return [void]
18
+ def in_process_query(object, collector)
19
+ docstring_text = object.docstring.to_s
20
+ return if docstring_text.empty?
21
+
22
+ errors = []
23
+
24
+ # Check for unclosed backticks
25
+ backtick_count = docstring_text.scan(/`/).count
26
+ errors << 'unclosed_backtick' if backtick_count.odd?
27
+
28
+ # Check for unclosed code blocks
29
+ code_block_count = docstring_text.scan(/^```/).count
30
+ errors << 'unclosed_code_block' if code_block_count.odd?
31
+
32
+ # Check for unclosed bold markers (excluding code sections)
33
+ non_code_text = docstring_text.gsub(/`[^`]*`/, '')
34
+ bold_count = non_code_text.scan(/\*\*/).count
35
+ errors << 'unclosed_bold' if bold_count.odd?
36
+
37
+ # Check for invalid list markers
38
+ docstring_text.lines.each_with_index do |line, line_idx|
39
+ stripped = line.strip
40
+ errors << "invalid_list_marker:#{line_idx + 1}" if stripped.match?(/^[•·]/)
41
+ end
42
+
43
+ return if errors.empty?
17
44
 
18
- # Builds and executes the YARD command to detect markdown syntax errors
19
- # @param dir [String] the directory containing the .yardoc database
20
- # @param file_list_path [String] path to file containing list of files to analyze
21
- # @return [String] command output
22
- def yard_cmd(dir, file_list_path)
23
- cmd = <<~CMD
24
- cat #{Shellwords.escape(file_list_path)} | xargs yard list \
25
- #{shell_arguments} \
26
- --query #{query} \
27
- -q \
28
- -b #{Shellwords.escape(dir)}
29
- CMD
30
- cmd = cmd.tr("\n", ' ')
31
- shell(cmd)
45
+ collector.puts "#{object.file}:#{object.line}: #{object.title}"
46
+ collector.puts errors.join('|')
32
47
  end
33
48
  end
34
49
  end
@@ -28,7 +28,6 @@ module Yard
28
28
  #
29
29
  # Documentation/MarkdownSyntax:
30
30
  # Enabled: false
31
- #
32
31
  module MarkdownSyntax
33
32
  end
34
33
  end