rubocop-yardoc 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.
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Yardoc
6
+ # Ensures there are blank comment lines between blocks of tags in
7
+ # documentation comments.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # # Title
12
+ # # @param x [Integer] desc
13
+ # # @param y [Integer] desc
14
+ # # @return [void]
15
+ # def my_method(x, y); end
16
+ #
17
+ # # good
18
+ # # Title
19
+ # #
20
+ # # @param x [Integer] desc
21
+ # # @param y [Integer] desc
22
+ # #
23
+ # # @return [void]
24
+ # def my_method(x, y); end
25
+ #
26
+ class SeparateTagsBlocks < Base
27
+ include RangeHelp
28
+ extend AutoCorrector
29
+
30
+ # Represents a comment entry (it may be a multi-line comment)
31
+ #
32
+ # @return [Struct] With :comment, :text, :tag, :line, :blank?
33
+ CommentEntry = Struct.new(:comment, :text, :tag, :line, :blank, keyword_init: true) do
34
+ # Whether the comment is empty
35
+ #
36
+ # @return [Boolean]
37
+ def blank?
38
+ blank
39
+ end
40
+ end
41
+
42
+ MSG = 'Add a blank comment line between different tag blocks.'
43
+
44
+ # Matches a comment line: captures the text after `#`.
45
+ # `#` with only trailing whitespace is a "blank comment line".
46
+ COMMENT_TEXT_RE = /\A#\s?(.*)\z/
47
+
48
+ # Matches a YARD-style tag at the start of comment text.
49
+ TAG_RE = /\A@(\w+)/
50
+
51
+ # Executed for each method definition
52
+ #
53
+ # @param node [RuboCop::AST::Node] The AST node
54
+ def on_def(node)
55
+ return unless documented? node
56
+
57
+ doc_comment_groups.each do |group|
58
+ check_group(group)
59
+ end
60
+ end
61
+
62
+ alias on_defs on_def
63
+
64
+ private
65
+
66
+ # Returns an array of "groups", where each group is an array of
67
+ # consecutive comment lines that form a single doc comment block.
68
+ def doc_comment_groups # rubocop:disable Metrics/MethodLength
69
+ groups = []
70
+ current = []
71
+
72
+ processed_source.comments.each do |comment|
73
+ # Only look at `#` style comments (not block =begin/=end)
74
+ next unless comment.text.start_with?('#')
75
+
76
+ if current.empty? || consecutive?(current.last, comment)
77
+ current << comment
78
+ else
79
+ groups << current
80
+ current = [comment]
81
+ end
82
+ end
83
+
84
+ groups << current unless current.empty?
85
+ groups
86
+ end
87
+
88
+ # True if `comment` is on the very next line after `prev`.
89
+ #
90
+ # @param prev [Parser::Source::Comment] Previous comment
91
+ # @param comment [Parser::Source::Comment] Current comment
92
+ #
93
+ # @return [Boolean]
94
+ def consecutive?(prev, comment)
95
+ comment.loc.line == prev.loc.line + 1
96
+ end
97
+
98
+ # Adds offenses for issues found in `comments`
99
+ #
100
+ # @param comments [Array<Parser::Source::Comment>] List of comments to check
101
+ def check_group(comments) # rubocop:disable Metrics/MethodLength
102
+ parsed = comments.map { |c| parse_comment(c) }
103
+
104
+ prev_tag = nil
105
+ prev_line_blank = false
106
+
107
+ parsed.each_with_index do |entry, idx|
108
+ if idx.positive? &&
109
+ entry.tag != prev_tag && !prev_line_blank &&
110
+ entry.tag != :text
111
+ # Boundary between two different blocks without a blank separator.
112
+ register_offense(comments[idx])
113
+ end
114
+
115
+ prev_tag = entry.tag
116
+ prev_line_blank = entry.blank?
117
+ end
118
+ end
119
+
120
+ # Makes an instance of CommentEntry from a comment
121
+ #
122
+ # @param comment [Parser::Source::Comment] Parsed source comment
123
+ #
124
+ # @return [CommentEntry] Nicer struct
125
+ def parse_comment(comment)
126
+ raw = comment.text
127
+ match = raw.match(COMMENT_TEXT_RE)
128
+ text = match ? match[1] : raw
129
+
130
+ blank = text.strip.empty?
131
+ tag_match = text.match(TAG_RE)
132
+ tag = tag_match ? tag_match[1].to_sym : :text
133
+
134
+ CommentEntry.new(comment: comment, text: text, tag: tag, line: comment.loc.line, blank: blank)
135
+ end
136
+
137
+ # Adds an offense for the given comment
138
+ #
139
+ # @param comment [Parser::Source::Comment] Offending comment
140
+ def register_offense(comment)
141
+ range = comment.loc.expression
142
+ add_offense(range) do |corrector|
143
+ indent = leading_indent(comment)
144
+ corrector.insert_before(range, "#\n#{indent}")
145
+ end
146
+ end
147
+
148
+ # Determine the indentation of the comment so the inserted blank line matches.
149
+ #
150
+ # @param comment [Parser::Source::Comment] Offending comment
151
+ #
152
+ # @return [String] Indented comment
153
+ def leading_indent(comment)
154
+ line_source = processed_source.lines[comment.loc.line - 1] || ''
155
+
156
+ line_source[/\A\s*/] || ''
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Yardoc
6
+ # Ensures only supported YARD tags are used.
7
+ #
8
+ # The built-in supported tags follow the official YARD tag list.
9
+ #
10
+ # Additional tags can be whitelisted via `AdditionalTags` if they are
11
+ # not properly registered in Yardoc
12
+ # See Adding Custom Tags: https://rubydoc.info/gems/yard/file/docs/TagsArch.md#Adding_Custom_Tags
13
+ #
14
+ # Meta tags are not supported (`@!<tag>`, used when metaprogramming).
15
+ #
16
+ # @example
17
+ # # bad
18
+ # # @unknown_tag some value
19
+ # def foo; end
20
+ #
21
+ # # good
22
+ # # @param name [String] a name
23
+ # # @return [void]
24
+ # def foo(name); end
25
+ #
26
+ # @example AdditionalTags: ['custom_tag']
27
+ # # good
28
+ # # @custom_tag some value
29
+ # def foo; end
30
+ class SupportedTags < Base
31
+ include ParamHelp
32
+ include YardHelp
33
+
34
+ # List of the supported tags
35
+ #
36
+ # @see https://rubydoc.info/gems/yard/file/docs/Tags.md#Tag_List
37
+ YARD_TAGS = %w[
38
+ abstract api attr attr_reader attr_writer author deprecated example
39
+ note option overload param private raise return see since todo type
40
+ version yield yieldparam yieldreturn
41
+ ].freeze
42
+
43
+ MSG = 'Unsupported YARD tag `@%<tag>s`. Supported tags: %<supported>s.'
44
+
45
+ # Executed for each module/class/method definition (with aliases)
46
+ #
47
+ # @param node [RuboCop::AST::Node] The AST node
48
+ def on_def(node)
49
+ docstring = yard_docstring(node)
50
+ return if docstring.nil?
51
+
52
+ # Valid Yardoc tags (and registered ones if any)
53
+ yardoc_tags = docstring.tags.map(&:tag_name)
54
+
55
+ tags_and_positions(node).filter do |pair|
56
+ next if legit_or_ignored_tag?(yardoc_tags, pair[:tag])
57
+
58
+ add_offense pair[:range], message: format(MSG, tag: pair[:tag], supported: supported_tags.join(', '))
59
+ end
60
+ end
61
+
62
+ alias on_defs on_def
63
+ alias on_module on_def
64
+ alias on_class on_def
65
+
66
+ private
67
+
68
+ # List of Yard allowed tags and additional ones
69
+ def supported_tags
70
+ YARD_TAGS + Array(cop_config['AdditionalTags'])
71
+ end
72
+
73
+ # Invalid Yardoc tags still allowed
74
+ def additional_tags
75
+ Array(cop_config['AdditionalTags'])
76
+ end
77
+
78
+ # Checks a tag against tags found by Yardoc and the ignore list
79
+ #
80
+ # @param yardoc_tags [Array<String>] List of tags extracted from the source
81
+ # @param tag [String] Tag to check
82
+ #
83
+ # @return [Boolean]
84
+ def legit_or_ignored_tag?(yardoc_tags, tag)
85
+ yardoc_tags.include?(tag) || ignored?(tag, additional_tags)
86
+ end
87
+
88
+ # Extract all tags and their position from the comment block
89
+ #
90
+ # @param node [RuboCop::AST::Node] The AST node to find docs for
91
+ # @param tag_name [String, nil] Optional tag to filter
92
+ def tags_and_positions(node, tag_name = nil)
93
+ node_comments = processed_source.ast_with_comments.fetch(node, [])
94
+
95
+ node_comments.filter_map do |comment|
96
+ text = comment.text
97
+ tag = text[/^# @((\w+)[\w\d]+)/, 1]
98
+ next if tag.nil? || (!tag_name.nil? && tag_name != tag)
99
+
100
+ {
101
+ tag: tag,
102
+ range: start_tag_position(comment, tag),
103
+ }
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Yardoc
6
+ # Ensures YARD tags appear in the configured order.
7
+ #
8
+ # Override with the `Order` config option.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # # @return [void]
13
+ # # @param name [String] a name
14
+ # def foo(name); end
15
+ #
16
+ # # good
17
+ # # @param name [String] a name
18
+ # # @return [void]
19
+ # def foo(name); end
20
+ #
21
+ # @example Order: [return, param]
22
+ # # bad
23
+ # # @param name [String] a name
24
+ # # @return [void]
25
+ # def foo(name); end
26
+ #
27
+ # # good
28
+ # # @return [void]
29
+ # # @param name [String] a name
30
+ # def foo(name); end
31
+ class TagOrder < Base
32
+ include YardHelp
33
+
34
+ MSG = 'YARD tags are out of order. Expected order: %<order>s. Found `@%<current>s` after `@%<previous>s`.'
35
+
36
+ # Default expected order corresponding to the "Order" option
37
+ DEFAULT_ORDER = %w[param option yieldparam yieldreturn return raise see since deprecated note example].freeze
38
+
39
+ # Executed for every module/class/method definition (with aliases)
40
+ #
41
+ # @param node [RuboCop::AST::Node] The AST node
42
+ def on_def(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
43
+ tags = yard_tags(node)
44
+ return if tags.empty?
45
+
46
+ tag_names = tags.map(&:tag_name).select { |t| order.include?(t) }
47
+
48
+ tag_names.each_cons(2) do |prev_tag, curr_tag|
49
+ prev_idx = order.index(prev_tag)
50
+ curr_idx = order.index(curr_tag)
51
+
52
+ # Unknown tags (not in order list) are skipped
53
+ next if prev_idx.nil? || curr_idx.nil?
54
+ next if curr_idx >= prev_idx
55
+
56
+ add_offense(
57
+ node,
58
+ message: format(
59
+ MSG,
60
+ order: order.map { |t| "@#{t}" }.join(', '),
61
+ current: curr_tag,
62
+ previous: prev_tag
63
+ )
64
+ )
65
+
66
+ break
67
+ end
68
+ end
69
+
70
+ alias on_defs on_def
71
+ alias on_module on_def
72
+ alias on_class on_def
73
+
74
+ private
75
+
76
+ # Order configuration
77
+ def order
78
+ cop_config.fetch('Order', DEFAULT_ORDER)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Yardoc
6
+ # Ensures methods that yield a block document it with @yield.
7
+ #
8
+ # Also checks for @yieldparam when the yield passes arguments,
9
+ # and @yieldreturn when the yield's return value is used.
10
+ #
11
+ # @example RequireParamDocumentation: true (default)
12
+ # # bad
13
+ # def each
14
+ # yield item
15
+ # end
16
+ #
17
+ # # good
18
+ # # @yield [item] iterates over items
19
+ # # @yieldparam item [Object] the current item
20
+ # def each
21
+ # yield item
22
+ # end
23
+ #
24
+ # @example RequireParamDocumentation: false
25
+ # # bad (no yield documentation)
26
+ # def each
27
+ # yield item
28
+ # end
29
+ #
30
+ # # good (ignored missing param documentation)
31
+ # # @yield [item] iterates over items
32
+ # def each
33
+ # yield item
34
+ # end
35
+ class YieldDocumentation < Base
36
+ include YardHelp
37
+
38
+ MSG_YIELD = 'Method yields a block but is missing a @yield tag.'
39
+ MSG_YIELDPARAM = 'Method yields with arguments but is missing @yieldparam tags.'
40
+
41
+ # Executed for every method definition
42
+ #
43
+ # @param node [RuboCop::AST::Node] The AST node
44
+ def on_def(node)
45
+ return unless yields?(node)
46
+ return unless documented?(node)
47
+
48
+ tags = yard_tags(node)
49
+
50
+ unless tags.any? { |t| t.tag_name == 'yield' }
51
+ add_offense(node, message: MSG_YIELD)
52
+ return
53
+ end
54
+
55
+ check_yieldparams(node, tags) if require_param_documentation?
56
+ end
57
+
58
+ alias on_defs on_def
59
+
60
+ private
61
+
62
+ # Whether the node yields something in its body
63
+ #
64
+ # @param node [RuboCop::AST::Node] The AST node
65
+ #
66
+ # @return [Boolean]
67
+ def yields?(node)
68
+ node.each_descendant(:yield).any?
69
+ end
70
+
71
+ # Whether the documntation is needed for parameters
72
+ #
73
+ # @return [Boolean]
74
+ def require_param_documentation?
75
+ cop_config.fetch('RequireParamDocumentation', true)
76
+ end
77
+
78
+ # Checks if current node uses a block parameter an has a "yieldparams" tag
79
+ #
80
+ # @param node [RuboCop::AST::Node] The AST node
81
+ # @param tags [Array<YARD::Tags::Tag>] Tags of the node
82
+ def check_yieldparams(node, tags)
83
+ yield_args = node.each_descendant(:yield).flat_map(&:children)
84
+ return if yield_args.empty?
85
+
86
+ yieldparam_tags = tags.select { |t| t.tag_name == 'yieldparam' }
87
+ return if yieldparam_tags.size >= yield_args.size
88
+
89
+ add_offense(node, message: MSG_YIELDPARAM)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mixin/node_name_help'
4
+ require_relative 'mixin/param_help'
5
+ require_relative 'mixin/yard_help'
6
+
7
+ require_relative 'yardoc/base'
8
+
9
+ require_relative 'yardoc/class_description'
10
+ require_relative 'yardoc/column_params'
11
+ require_relative 'yardoc/constant_description'
12
+ require_relative 'yardoc/method_description'
13
+ require_relative 'yardoc/module_description'
14
+ require_relative 'yardoc/param_description_casing'
15
+ require_relative 'yardoc/param_documentation'
16
+ require_relative 'yardoc/separate_tags_blocks'
17
+ require_relative 'yardoc/supported_tags'
18
+ require_relative 'yardoc/tag_order'
19
+ require_relative 'yardoc/yield_documentation'
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lint_roller'
4
+ require_relative 'version'
5
+
6
+ module RuboCop
7
+ module Yardoc
8
+ # A plugin that integrates RuboCop Yardoc with RuboCop's plugin system.
9
+ class Plugin < LintRoller::Plugin
10
+ # Information about the plugin
11
+ def about
12
+ LintRoller::About.new(
13
+ name: 'rubocop-yardoc',
14
+ version: RuboCop::Yardoc::VERSION,
15
+ homepage: '__FIXME__',
16
+ description: 'A collection of RuboCop cops to check for Yardoc documentation.'
17
+ )
18
+ end
19
+
20
+ # Whether the plugin can run in given context
21
+ #
22
+ # @param context [LintRoller::Context] Execution context
23
+ def supported?(context)
24
+ context.engine == :rubocop
25
+ end
26
+
27
+ # Rules configuration
28
+ #
29
+ # @param _context [LintRoller::Context] Execution context
30
+ def rules(_context)
31
+ project_root = Pathname.new(__dir__).join('../../..')
32
+
33
+ ConfigObsoletion.files << project_root.join('config', 'obsoletion.yml')
34
+
35
+ LintRoller::Rules.new(type: :path, config_format: :rubocop, value: project_root.join('config', 'default.yml'))
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Yardoc
5
+ # Library version
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ # RuboCop Yardoc project namespace.
5
+ module Yardoc
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+ require 'yard'
5
+
6
+ require_relative 'rubocop/yardoc'
7
+ require_relative 'rubocop/yardoc/version'
8
+ require_relative 'rubocop/yardoc/plugin'
9
+ require_relative 'rubocop/cop/yardoc_cops'
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubocop-yardoc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Manuel Tancoigne
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-08-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: lint_roller
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '1.72'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '2.0'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '1.72'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: yard
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0.9'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '1.0'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0.9'
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '1.0'
67
+ description: Provides a set of RuboCop rules to enforce comments format and improve
68
+ documentation
69
+ email:
70
+ - manu@experimentslabs.com
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - CHANGELOG.md
76
+ - CODE_OF_CONDUCT.md
77
+ - CONTRIBUTING.md
78
+ - LICENSE
79
+ - README.md
80
+ - config/default.yml
81
+ - config/obsoletion.yml
82
+ - lib/rubocop-yardoc.rb
83
+ - lib/rubocop/cop/mixin/node_name_help.rb
84
+ - lib/rubocop/cop/mixin/param_help.rb
85
+ - lib/rubocop/cop/mixin/yard_help.rb
86
+ - lib/rubocop/cop/yardoc/base.rb
87
+ - lib/rubocop/cop/yardoc/class_description.rb
88
+ - lib/rubocop/cop/yardoc/column_params.rb
89
+ - lib/rubocop/cop/yardoc/constant_description.rb
90
+ - lib/rubocop/cop/yardoc/method_description.rb
91
+ - lib/rubocop/cop/yardoc/module_description.rb
92
+ - lib/rubocop/cop/yardoc/param_description_casing.rb
93
+ - lib/rubocop/cop/yardoc/param_documentation.rb
94
+ - lib/rubocop/cop/yardoc/separate_tags_blocks.rb
95
+ - lib/rubocop/cop/yardoc/supported_tags.rb
96
+ - lib/rubocop/cop/yardoc/tag_order.rb
97
+ - lib/rubocop/cop/yardoc/yield_documentation.rb
98
+ - lib/rubocop/cop/yardoc_cops.rb
99
+ - lib/rubocop/yardoc.rb
100
+ - lib/rubocop/yardoc/plugin.rb
101
+ - lib/rubocop/yardoc/version.rb
102
+ homepage: https://gitlab.com/experimentslabs/rubocop-yardoc
103
+ licenses: []
104
+ metadata:
105
+ licenses: MIT
106
+ homepage_uri: https://gitlab.com/experimentslabs/rubocop-yardoc
107
+ source_code_uri: https://gitlab.com/experimentslabs/rubocop-yardoc
108
+ changelog_uri: https://gitlab.com/experimentslabs/rubocop-yardoc/-/blob/main/README.md
109
+ default_lint_roller_plugin: RuboCop::Yardoc::Plugin
110
+ rubygems_mfa_required: 'true'
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: 3.3.0
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubygems_version: 3.5.22
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: RuboCop plugin to enforce comments format
130
+ test_files: []