yard-lint 1.2.3 → 1.3.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 (102) 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/ext/irb_notifier_shim.rb +19 -6
  14. data/lib/yard/lint/results/base.rb +2 -1
  15. data/lib/yard/lint/runner.rb +50 -38
  16. data/lib/yard/lint/templates/default_config.yml +105 -0
  17. data/lib/yard/lint/templates/strict_config.yml +105 -0
  18. data/lib/yard/lint/validators/base.rb +52 -118
  19. data/lib/yard/lint/validators/documentation/blank_line_before_definition/config.rb +25 -0
  20. data/lib/yard/lint/validators/documentation/blank_line_before_definition/messages_builder.rb +39 -0
  21. data/lib/yard/lint/validators/documentation/blank_line_before_definition/parser.rb +59 -0
  22. data/lib/yard/lint/validators/documentation/blank_line_before_definition/result.rb +61 -0
  23. data/lib/yard/lint/validators/documentation/blank_line_before_definition/validator.rb +94 -0
  24. data/lib/yard/lint/validators/documentation/blank_line_before_definition.rb +63 -0
  25. data/lib/yard/lint/validators/documentation/empty_comment_line/config.rb +24 -0
  26. data/lib/yard/lint/validators/documentation/empty_comment_line/messages_builder.rb +34 -0
  27. data/lib/yard/lint/validators/documentation/empty_comment_line/parser.rb +60 -0
  28. data/lib/yard/lint/validators/documentation/empty_comment_line/result.rb +25 -0
  29. data/lib/yard/lint/validators/documentation/empty_comment_line/validator.rb +109 -0
  30. data/lib/yard/lint/validators/documentation/empty_comment_line.rb +58 -0
  31. data/lib/yard/lint/validators/documentation/markdown_syntax/validator.rb +36 -21
  32. data/lib/yard/lint/validators/documentation/markdown_syntax.rb +0 -1
  33. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/validator.rb +19 -29
  34. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods.rb +0 -1
  35. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/validator.rb +18 -34
  36. data/lib/yard/lint/validators/documentation/undocumented_method_arguments.rb +0 -1
  37. data/lib/yard/lint/validators/documentation/undocumented_objects/validator.rb +17 -25
  38. data/lib/yard/lint/validators/documentation/undocumented_objects.rb +4 -5
  39. data/lib/yard/lint/validators/documentation/undocumented_options/validator.rb +30 -21
  40. data/lib/yard/lint/validators/documentation/undocumented_options.rb +0 -1
  41. data/lib/yard/lint/validators/semantic/abstract_methods/result.rb +2 -2
  42. data/lib/yard/lint/validators/semantic/abstract_methods/validator.rb +31 -43
  43. data/lib/yard/lint/validators/semantic/abstract_methods.rb +0 -1
  44. data/lib/yard/lint/validators/tags/api_tags/validator.rb +24 -39
  45. data/lib/yard/lint/validators/tags/api_tags.rb +0 -1
  46. data/lib/yard/lint/validators/tags/collection_type/validator.rb +37 -66
  47. data/lib/yard/lint/validators/tags/collection_type.rb +0 -1
  48. data/lib/yard/lint/validators/tags/example_syntax/validator.rb +51 -64
  49. data/lib/yard/lint/validators/tags/example_syntax.rb +0 -1
  50. data/lib/yard/lint/validators/tags/informal_notation/config.rb +40 -0
  51. data/lib/yard/lint/validators/tags/informal_notation/messages_builder.rb +35 -0
  52. data/lib/yard/lint/validators/tags/informal_notation/parser.rb +55 -0
  53. data/lib/yard/lint/validators/tags/informal_notation/result.rb +26 -0
  54. data/lib/yard/lint/validators/tags/informal_notation/validator.rb +133 -0
  55. data/lib/yard/lint/validators/tags/informal_notation.rb +45 -0
  56. data/lib/yard/lint/validators/tags/invalid_types/validator.rb +57 -70
  57. data/lib/yard/lint/validators/tags/invalid_types.rb +0 -1
  58. data/lib/yard/lint/validators/tags/meaningless_tag/validator.rb +22 -54
  59. data/lib/yard/lint/validators/tags/meaningless_tag.rb +0 -1
  60. data/lib/yard/lint/validators/tags/non_ascii_type/config.rb +21 -0
  61. data/lib/yard/lint/validators/tags/non_ascii_type/messages_builder.rb +29 -0
  62. data/lib/yard/lint/validators/tags/non_ascii_type/parser.rb +59 -0
  63. data/lib/yard/lint/validators/tags/non_ascii_type/result.rb +25 -0
  64. data/lib/yard/lint/validators/tags/non_ascii_type/validator.rb +50 -0
  65. data/lib/yard/lint/validators/tags/non_ascii_type.rb +39 -0
  66. data/lib/yard/lint/validators/tags/option_tags/result.rb +2 -2
  67. data/lib/yard/lint/validators/tags/option_tags/validator.rb +25 -40
  68. data/lib/yard/lint/validators/tags/option_tags.rb +0 -1
  69. data/lib/yard/lint/validators/tags/order/validator.rb +28 -55
  70. data/lib/yard/lint/validators/tags/order.rb +0 -1
  71. data/lib/yard/lint/validators/tags/redundant_param_description/config.rb +15 -1
  72. data/lib/yard/lint/validators/tags/redundant_param_description/messages_builder.rb +5 -0
  73. data/lib/yard/lint/validators/tags/redundant_param_description/validator.rb +134 -100
  74. data/lib/yard/lint/validators/tags/redundant_param_description.rb +0 -1
  75. data/lib/yard/lint/validators/tags/tag_group_separator/config.rb +29 -0
  76. data/lib/yard/lint/validators/tags/tag_group_separator/messages_builder.rb +49 -0
  77. data/lib/yard/lint/validators/tags/tag_group_separator/parser.rb +67 -0
  78. data/lib/yard/lint/validators/tags/tag_group_separator/result.rb +28 -0
  79. data/lib/yard/lint/validators/tags/tag_group_separator/validator.rb +117 -0
  80. data/lib/yard/lint/validators/tags/tag_group_separator.rb +49 -0
  81. data/lib/yard/lint/validators/tags/tag_type_position/validator.rb +53 -84
  82. data/lib/yard/lint/validators/tags/tag_type_position.rb +0 -1
  83. data/lib/yard/lint/validators/tags/type_syntax/parser.rb +7 -2
  84. data/lib/yard/lint/validators/tags/type_syntax/validator.rb +29 -59
  85. data/lib/yard/lint/validators/tags/type_syntax.rb +0 -1
  86. data/lib/yard/lint/validators/warnings/duplicated_parameter_name/validator.rb +1 -18
  87. data/lib/yard/lint/validators/warnings/invalid_directive_format/validator.rb +1 -18
  88. data/lib/yard/lint/validators/warnings/invalid_tag_format/validator.rb +1 -18
  89. data/lib/yard/lint/validators/warnings/unknown_directive/validator.rb +1 -18
  90. data/lib/yard/lint/validators/warnings/unknown_parameter_name/messages_builder.rb +243 -0
  91. data/lib/yard/lint/validators/warnings/unknown_parameter_name/result.rb +4 -3
  92. data/lib/yard/lint/validators/warnings/unknown_parameter_name/validator.rb +1 -18
  93. data/lib/yard/lint/validators/warnings/unknown_tag/messages_builder.rb +144 -0
  94. data/lib/yard/lint/validators/warnings/unknown_tag/result.rb +4 -3
  95. data/lib/yard/lint/validators/warnings/unknown_tag/validator.rb +1 -18
  96. data/lib/yard/lint/validators/warnings/unknown_tag.rb +10 -0
  97. data/lib/yard/lint/version.rb +1 -1
  98. data/lib/yard/lint.rb +81 -13
  99. data/lib/yard-lint.rb +1 -1
  100. data/renovate.json +1 -8
  101. metadata +38 -2
  102. data/lib/yard/lint/command_cache.rb +0 -93
@@ -7,112 +7,136 @@ module Yard
7
7
  module RedundantParamDescription
8
8
  # Validates that parameter descriptions are not redundant/meaningless
9
9
  class Validator < Validators::Base
10
- # YARD query to detect redundant parameter descriptions
11
- # @return [String] YARD Ruby query code
12
- def query
13
- articles = config_articles.join('|')
14
- generic_terms = config_generic_terms.join('|')
10
+ # Enable in-process execution
11
+ in_process visibility: :public
12
+
13
+ # Execute query for a single object during in-process execution.
14
+ # Checks for redundant/meaningless parameter descriptions.
15
+ # @param object [YARD::CodeObjects::Base] the code object to query
16
+ # @param collector [Executor::ResultCollector] collector for output
17
+ # @return [void]
18
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
19
+ def in_process_query(object, collector)
20
+ return unless object.is_a?(YARD::CodeObjects::MethodObject)
21
+
22
+ articles = config_articles
23
+ generic_terms = config_generic_terms.map(&:downcase)
24
+ connectors = config_low_value_connectors.map(&:downcase)
25
+ low_value_verbs = config_low_value_verbs.map(&:downcase)
15
26
  max_words = config_max_redundant_words
16
- checked_tags = config_checked_tags
27
+ tags_to_check = config_checked_tags
17
28
  patterns = config_enabled_patterns
18
29
 
19
- # Build query as single line for shell compatibility
20
- query_body = 'if object.is_a?(YARD::CodeObjects::MethodObject); ' \
21
- "object.docstring.tags.select { |tag| #{checked_tags.inspect}.include?(tag.tag_name) }.each do |tag|; " \
22
- 'next unless tag.name && tag.text && !tag.text.strip.empty?; ' \
23
- 'param_name = tag.name; ' \
24
- 'description = tag.text.strip.gsub(/\\.$/, ""); ' \
25
- 'word_count = description.split.length; ' \
26
- 'type_name = tag.types&.first&.gsub(/[<>{}\\[\\],]/, "")&.strip; ' \
27
- "next if word_count > #{max_words}; " \
28
- 'pattern_type = nil; ' \
29
- "if #{patterns['ArticleParam']} && word_count <= 3; " \
30
- "articles_re = /^(#{articles})/i; " \
31
- 'desc_parts = description.split; ' \
32
- 'if desc_parts.length == 2 && desc_parts[0].match?(articles_re) && desc_parts[1].downcase == param_name.downcase; ' \
33
- 'pattern_type = "article_param"; ' \
34
- 'end; ' \
35
- 'end; ' \
36
- "if pattern_type.nil? && #{patterns['PossessiveParam']} && word_count <= 4; " \
37
- 'desc_parts = description.split; ' \
38
- 'if desc_parts.length >= 3; ' \
39
- "articles_re = /^(#{articles})/i; " \
40
- 'if desc_parts[0].match?(articles_re) && desc_parts[1].end_with?("s") && desc_parts[1].include?(39.chr) && desc_parts[2].downcase == param_name.downcase; ' \
41
- 'pattern_type = "possessive_param"; ' \
42
- 'end; ' \
43
- 'end; ' \
44
- 'end; ' \
45
- "if pattern_type.nil? && #{patterns['TypeRestatement']} && type_name && word_count <= 2; " \
46
- "generic_terms_arr = [\"#{generic_terms.gsub('|', '\", \"')}\"].map(&:downcase); " \
47
- 'if description.downcase == type_name.downcase; ' \
48
- 'pattern_type = "type_restatement"; ' \
49
- 'elsif word_count == 2; ' \
50
- 'parts = description.split; ' \
51
- 'if parts[0].downcase == type_name.downcase && generic_terms_arr.include?(parts[1].downcase); ' \
52
- 'pattern_type = "type_restatement"; ' \
53
- 'end; ' \
54
- 'end; ' \
55
- 'end; ' \
56
- "if pattern_type.nil? && #{patterns['ParamToVerb']} && word_count <= 4; " \
57
- 'parts = description.split; ' \
58
- 'if parts.length == 3 && parts[0].downcase == param_name.downcase && parts[1].downcase == "to"; ' \
59
- 'pattern_type = "param_to_verb"; ' \
60
- 'end; ' \
61
- 'end; ' \
62
- "if pattern_type.nil? && #{patterns['IdPattern']} && word_count <= 6; " \
63
- 'if param_name =~ /_id$|_uuid$|_identifier$/; ' \
64
- 'if description =~ /^(ID|Unique identifier|Identifier)\\s+(of|for)\\s+/i; ' \
65
- 'pattern_type = "id_pattern"; ' \
66
- 'end; ' \
67
- 'end; ' \
68
- 'end; ' \
69
- "if pattern_type.nil? && #{patterns['DirectionalDate']} && word_count <= 4; " \
70
- 'if param_name =~ /^(from|to|till|until)$/; ' \
71
- 'parts = description.split; ' \
72
- 'if parts.length == 3 && parts[0].downcase == param_name.downcase && parts[1].downcase == "this"; ' \
73
- 'pattern_type = "directional_date"; ' \
74
- 'end; ' \
75
- 'end; ' \
76
- 'end; ' \
77
- "if pattern_type.nil? && #{patterns['TypeGeneric']} && type_name && word_count <= 5; " \
78
- "generic_terms_arr = [\"#{generic_terms.gsub('|', '\", \"')}\"].map(&:downcase); " \
79
- 'parts = description.split; ' \
80
- 'if parts.length >= 2 && parts[0].downcase == type_name.downcase && generic_terms_arr.include?(parts[1].downcase); ' \
81
- 'pattern_type = "type_generic"; ' \
82
- 'end; ' \
83
- 'end; ' \
84
- 'if pattern_type; ' \
85
- 'puts object.file + ":" + object.line.to_s + ": " + object.title; ' \
86
- 'puts tag.tag_name + "|" + param_name + "|" + tag.text.strip + "|" + (type_name || "") + "|" + pattern_type + "|" + word_count.to_s; ' \
87
- 'end; ' \
88
- 'end; ' \
89
- 'end; ' \
90
- 'false'
91
-
92
- # Wrap in single quotes like other validators do
93
- "'#{query_body}'"
30
+ object.docstring.tags.each do |tag|
31
+ next unless tags_to_check.include?(tag.tag_name)
32
+ next unless tag.name && tag.text && !tag.text.strip.empty?
33
+
34
+ param_name = tag.name
35
+ description = tag.text.strip.gsub(/\.$/, '')
36
+ word_count = description.split.length
37
+ type_name = tag.types&.first&.gsub(/[<>{}\[\],]/, '')&.strip
38
+
39
+ next if word_count > max_words
40
+
41
+ pattern_type = detect_pattern(
42
+ param_name, description, type_name, word_count,
43
+ articles, generic_terms, connectors, low_value_verbs, patterns
44
+ )
45
+
46
+ next unless pattern_type
47
+
48
+ collector.puts "#{object.file}:#{object.line}: #{object.title}"
49
+ collector.puts "#{tag.tag_name}|#{param_name}|#{tag.text.strip}|#{type_name || ''}|#{pattern_type}|#{word_count}"
50
+ end
94
51
  end
52
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
53
+
54
+ private
55
+
56
+ # Detect the type of redundant pattern
57
+ # @param param_name [String] parameter name
58
+ # @param description [String] description text
59
+ # @param type_name [String, nil] type annotation
60
+ # @param word_count [Integer] number of words in description
61
+ # @param articles [Array<String>] article words
62
+ # @param generic_terms [Array<String>] generic terms
63
+ # @param connectors [Array<String>] low-value connectors
64
+ # @param low_value_verbs [Array<String>] low-value verbs
65
+ # @param patterns [Hash] enabled pattern flags
66
+ # @return [String, nil] pattern type or nil
67
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/ParameterLists
68
+ def detect_pattern(param_name, description, type_name, word_count, articles, generic_terms, connectors, low_value_verbs, patterns)
69
+ desc_parts = description.split
70
+ articles_re = /^(#{articles.join('|')})/i
71
+
72
+ # ArticleParam pattern
73
+ if patterns['ArticleParam'] && word_count <= 3 && desc_parts.length == 2
74
+ if desc_parts[0].match?(articles_re) && desc_parts[1].downcase == param_name.downcase
75
+ return 'article_param'
76
+ end
77
+ end
95
78
 
96
- # Builds and executes the YARD command
97
- # @param dir [String] the directory containing the .yardoc database
98
- # @param file_list_path [String] path to file containing list of files to analyze
99
- # @return [Hash] command output with stdout, stderr, exit_code
100
- def yard_cmd(dir, file_list_path)
101
- # Create a temporary script file to avoid shell escaping issues
102
- require 'tempfile'
103
-
104
- script = Tempfile.new(['yard_lint_query', '.sh'])
105
- script.write("#!/bin/bash\n")
106
- # Write query to a variable - since query already has outer single quotes, just assign it
107
- script.write("QUERY=#{query}\n")
108
- script.write("cat #{Shellwords.escape(file_list_path)} | xargs yard list #{shell_arguments} --query \"$QUERY\" -q -b #{Shellwords.escape(dir)}\n")
109
- script.chmod(0o755)
110
- script.close
111
-
112
- result = shell(script.path)
113
- script.unlink
114
- result
79
+ # PossessiveParam pattern
80
+ if patterns['PossessiveParam'] && word_count <= 4 && desc_parts.length >= 3
81
+ if desc_parts[0].match?(articles_re) && desc_parts[1].end_with?('s') &&
82
+ desc_parts[1].include?("'") && desc_parts[2].downcase == param_name.downcase
83
+ return 'possessive_param'
84
+ end
85
+ end
86
+
87
+ # TypeRestatement pattern
88
+ if patterns['TypeRestatement'] && type_name && word_count <= 2
89
+ if description.downcase == type_name.downcase
90
+ return 'type_restatement'
91
+ elsif word_count == 2
92
+ parts = description.split
93
+ if parts[0].downcase == type_name.downcase && generic_terms.include?(parts[1].downcase)
94
+ return 'type_restatement'
95
+ end
96
+ end
97
+ end
98
+
99
+ # ParamToVerb pattern
100
+ if patterns['ParamToVerb'] && word_count <= 4 && desc_parts.length == 3
101
+ if desc_parts[0].downcase == param_name.downcase && desc_parts[1].downcase == 'to'
102
+ return 'param_to_verb'
103
+ end
104
+ end
105
+
106
+ # IdPattern
107
+ if patterns['IdPattern'] && word_count <= 6 && param_name =~ /_id$|_uuid$|_identifier$/
108
+ if description =~ /^(ID|Unique identifier|Identifier)\s+(of|for)\s+/i
109
+ return 'id_pattern'
110
+ end
111
+ end
112
+
113
+ # DirectionalDate pattern
114
+ if patterns['DirectionalDate'] && word_count <= 4 && param_name =~ /^(from|to|till|until)$/
115
+ if desc_parts.length == 3 && desc_parts[0].downcase == param_name.downcase && desc_parts[1].downcase == 'this'
116
+ return 'directional_date'
117
+ end
118
+ end
119
+
120
+ # TypeGeneric pattern
121
+ if patterns['TypeGeneric'] && type_name && word_count <= 5 && desc_parts.length >= 2
122
+ if desc_parts[0].downcase == type_name.downcase && generic_terms.include?(desc_parts[1].downcase)
123
+ return 'type_generic'
124
+ end
125
+ end
126
+
127
+ # ArticleParamPhrase pattern: "The action being performed"
128
+ if patterns['ArticleParamPhrase'] && word_count >= 3 && desc_parts.length >= 3 &&
129
+ desc_parts[0].match?(articles_re) &&
130
+ desc_parts[1].downcase == param_name.downcase &&
131
+ connectors.include?(desc_parts[2].downcase) &&
132
+ (desc_parts.length == 3 ||
133
+ (desc_parts.length == 4 && low_value_verbs.include?(desc_parts[3].downcase)))
134
+ return 'article_param_phrase'
135
+ end
136
+
137
+ nil
115
138
  end
139
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize, Metrics/ParameterLists
116
140
 
117
141
  private
118
142
 
@@ -126,6 +150,16 @@ module Yard
126
150
  config_or_default('GenericTerms')
127
151
  end
128
152
 
153
+ # @return [Array<String>] configured low-value connectors
154
+ def config_low_value_connectors
155
+ config_or_default('LowValueConnectors')
156
+ end
157
+
158
+ # @return [Array<String>] configured low-value verbs
159
+ def config_low_value_verbs
160
+ config_or_default('LowValueVerbs')
161
+ end
162
+
129
163
  # @return [Integer] maximum word count for redundant descriptions
130
164
  def config_max_redundant_words
131
165
  config_or_default('MaxRedundantWords')
@@ -159,7 +159,6 @@ module Yard
159
159
  #
160
160
  # Tags/RedundantParamDescription:
161
161
  # Enabled: false
162
- #
163
162
  module RedundantParamDescription
164
163
  end
165
164
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TagGroupSeparator
8
+ # Configuration for TagGroupSeparator validator
9
+ class Config < ::Yard::Lint::Validators::Config
10
+ self.id = :tag_group_separator
11
+ self.defaults = {
12
+ 'Enabled' => false,
13
+ 'Severity' => 'convention',
14
+ 'TagGroups' => {
15
+ 'param' => %w[param option],
16
+ 'return' => %w[return],
17
+ 'error' => %w[raise throws],
18
+ 'example' => %w[example],
19
+ 'meta' => %w[see note todo deprecated since version api],
20
+ 'yield' => %w[yield yieldparam yieldreturn]
21
+ },
22
+ 'RequireAfterDescription' => false
23
+ }.freeze
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TagGroupSeparator
8
+ # Builds messages for missing tag group separator offenses.
9
+ class MessagesBuilder
10
+ class << self
11
+ # Build message for missing tag group separator.
12
+ #
13
+ # @param offense [Hash] offense data with :method_name and :separators keys
14
+ #
15
+ # @return [String] formatted message
16
+ def call(offense)
17
+ transitions = parse_transitions(offense[:separators])
18
+
19
+ if transitions.size == 1
20
+ from, to = transitions.first
21
+ "The `#{offense[:method_name]}` is missing a blank line between " \
22
+ "`#{from}` and `#{to}` tag groups."
23
+ else
24
+ formatted = transitions.map { |from, to| "`#{from}` -> `#{to}`" }.join(', ')
25
+ "The `#{offense[:method_name]}` is missing blank lines between tag groups: " \
26
+ "#{formatted}."
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ # Parses transition string into array of [from, to] pairs.
33
+ #
34
+ # @param separators [String] string in format "from->to,from->to"
35
+ #
36
+ # @return [Array<Array<String>>] array of [from, to] pairs
37
+ def parse_transitions(separators)
38
+ separators
39
+ .to_s
40
+ .split(',')
41
+ .map { |transition| transition.split('->') }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TagGroupSeparator
8
+ # Parser for extracting tag group separator violations from raw validator output.
9
+ #
10
+ # @example Output format
11
+ # /path/to/file.rb:10: ClassName#method_name
12
+ # param->return,return->error
13
+ class Parser < Parsers::Base
14
+ # Regexp to extract only word and numeric parts of the location line
15
+ NORMALIZATION_REGEXP = /\w+/
16
+
17
+ private_constant :NORMALIZATION_REGEXP
18
+
19
+ # Parses raw validator output into structured offense data.
20
+ #
21
+ # @param yard_list [String] raw validator output string
22
+ #
23
+ # @return [Array<Hash>] array of hashes with offense details
24
+ def call(yard_list)
25
+ return [] if yard_list.nil? || yard_list.empty?
26
+
27
+ base_hash = {}
28
+
29
+ yard_list.split("\n").each_slice(2).each do |location, separators|
30
+ next if location.nil? || separators.nil?
31
+
32
+ key = normalize(location)
33
+
34
+ if separators == 'valid'
35
+ base_hash[key] = 'valid'
36
+ else
37
+ base_hash[key] ||= [location, separators]
38
+ end
39
+ end
40
+
41
+ base_hash.delete_if { |_key, value| value == 'valid' }
42
+ separator_data = base_hash.values.map(&:last)
43
+
44
+ Validators::Documentation::UndocumentedMethodArguments::Parser
45
+ .new
46
+ .call(base_hash.values.map(&:first).join("\n"))
47
+ .each.with_index { |element, index| element[:separators] = separator_data[index] }
48
+ end
49
+
50
+ private
51
+
52
+ # Normalizes a location line by extracting only word and numeric characters.
53
+ #
54
+ # @param location_line [String] full line with the location
55
+ #
56
+ # @return [String] normalized line without special characters
57
+ def normalize(location_line)
58
+ location_line
59
+ .scan(NORMALIZATION_REGEXP)
60
+ .join
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TagGroupSeparator
8
+ # Result object for tag group separator validation.
9
+ # Transforms parsed separator violations into offense objects.
10
+ class Result < Results::Base
11
+ self.default_severity = 'convention'
12
+ self.offense_type = 'method'
13
+ self.offense_name = 'MissingTagGroupSeparator'
14
+
15
+ # Build human-readable message for tag group separator offense.
16
+ #
17
+ # @param offense [Hash] offense data with :method_name and :separators keys
18
+ #
19
+ # @return [String] formatted message
20
+ def build_message(offense)
21
+ MessagesBuilder.call(offense)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TagGroupSeparator
8
+ # Validates that blank lines separate different groups of YARD documentation tags.
9
+ #
10
+ # This validator enforces visual separation between semantically different tag
11
+ # groups (e.g., @param tags should be separated from @return tags by a blank line).
12
+ class Validator < Base
13
+ # Enable in-process execution with all visibility
14
+ in_process visibility: :all
15
+
16
+ # Execute query for a single object during in-process execution.
17
+ # Checks if different tag groups are separated by blank lines.
18
+ #
19
+ # @param object [YARD::CodeObjects::Base] the code object to query
20
+ # @param collector [Executor::ResultCollector] collector for output
21
+ #
22
+ # @return [void]
23
+ def in_process_query(object, collector)
24
+ return if object.is_alias?
25
+
26
+ docstring = object.docstring.all
27
+ return if docstring.nil? || docstring.empty?
28
+
29
+ missing_separators = find_missing_separators(docstring)
30
+
31
+ collector.puts "#{object.file}:#{object.line}: #{object.title}"
32
+
33
+ if missing_separators.empty?
34
+ collector.puts 'valid'
35
+ else
36
+ collector.puts missing_separators.map { |s| "#{s[:from]}->#{s[:to]}" }.join(',')
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ # Find all locations where a blank line separator is missing between tag groups.
43
+ #
44
+ # @param docstring [String] the raw docstring content
45
+ #
46
+ # @return [Array<Hash>] array of hashes with :from and :to group names
47
+ def find_missing_separators(docstring)
48
+ lines = docstring.split("\n")
49
+ missing = []
50
+
51
+ previous_group = require_after_description? ? 'description' : nil
52
+ had_blank_line = true
53
+
54
+ lines.each do |line|
55
+ stripped = line.strip
56
+
57
+ if stripped.empty?
58
+ had_blank_line = true
59
+ next
60
+ end
61
+
62
+ if stripped.start_with?('@')
63
+ tag_name = stripped.match(/^@(\S+)/)&.captures&.first
64
+ next unless tag_name
65
+
66
+ current_group = group_for_tag(tag_name)
67
+
68
+ if previous_group && current_group != previous_group && !had_blank_line
69
+ missing << { from: previous_group, to: current_group }
70
+ end
71
+
72
+ previous_group = current_group
73
+ elsif previous_group.nil? && require_after_description?
74
+ # Non-tag, non-blank line (continuation or description)
75
+ # Only set description group if we haven't seen tags yet
76
+ previous_group = 'description'
77
+ end
78
+
79
+ had_blank_line = false
80
+ end
81
+
82
+ missing
83
+ end
84
+
85
+ # Determine which group a tag belongs to.
86
+ #
87
+ # @param tag_name [String] the tag name without the @ prefix
88
+ #
89
+ # @return [String] the group name, or the tag name itself if not in any group
90
+ def group_for_tag(tag_name)
91
+ tag_groups.each do |group_name, tags|
92
+ return group_name if tags.include?(tag_name)
93
+ end
94
+
95
+ # Tags not in any configured group are their own group
96
+ tag_name
97
+ end
98
+
99
+ # @return [Hash] configured tag groups
100
+ def tag_groups
101
+ @tag_groups ||= config.validator_config('Tags/TagGroupSeparator', 'TagGroups') ||
102
+ Config.defaults['TagGroups']
103
+ end
104
+
105
+ # @return [Boolean] whether to require separator after description
106
+ def require_after_description?
107
+ @require_after_description ||= config.validator_config(
108
+ 'Tags/TagGroupSeparator',
109
+ 'RequireAfterDescription'
110
+ ) || false
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ # TagGroupSeparator validator
8
+ #
9
+ # Enforces blank line separators between different groups of YARD documentation
10
+ # tags. This improves readability by visually grouping semantically related tags.
11
+ # This validator is disabled by default.
12
+ #
13
+ # @example Bad - No separator between @param and @return
14
+ # # @param organization_id [String] the organization ID
15
+ # # @param id [String] the pet ID
16
+ # # @return [Pet] the pet object
17
+ # def call(organization_id, id)
18
+ # end
19
+ #
20
+ # @example Good - Blank line separates tag groups
21
+ # # @param organization_id [String] the organization ID
22
+ # # @param id [String] the pet ID
23
+ # #
24
+ # # @return [Pet] the pet object
25
+ # def call(organization_id, id)
26
+ # end
27
+ #
28
+ # ## Configuration
29
+ #
30
+ # To enable this validator:
31
+ #
32
+ # Tags/TagGroupSeparator:
33
+ # Enabled: true
34
+ #
35
+ # To customize tag groups:
36
+ #
37
+ # Tags/TagGroupSeparator:
38
+ # Enabled: true
39
+ # TagGroups:
40
+ # param: [param, option]
41
+ # return: [return]
42
+ # error: [raise]
43
+ # yield: [yield, yieldparam, yieldreturn]
44
+ module TagGroupSeparator
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end