yard-lint 0.2.1 → 1.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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -1
  3. data/README.md +121 -89
  4. data/bin/yard-lint +44 -0
  5. data/lib/yard/lint/config.rb +35 -3
  6. data/lib/yard/lint/config_generator.rb +191 -0
  7. data/lib/yard/lint/config_loader.rb +1 -1
  8. data/lib/yard/lint/result_builder.rb +10 -1
  9. data/lib/yard/lint/validators/base.rb +77 -12
  10. data/lib/yard/lint/validators/documentation/markdown_syntax/config.rb +20 -0
  11. data/lib/yard/lint/validators/documentation/markdown_syntax/messages_builder.rb +44 -0
  12. data/lib/yard/lint/validators/documentation/markdown_syntax/parser.rb +53 -0
  13. data/lib/yard/lint/validators/documentation/markdown_syntax/result.rb +25 -0
  14. data/lib/yard/lint/validators/documentation/markdown_syntax/validator.rb +38 -0
  15. data/lib/yard/lint/validators/documentation/markdown_syntax.rb +37 -0
  16. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods/validator.rb +7 -6
  17. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods.rb +26 -1
  18. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/validator.rb +4 -5
  19. data/lib/yard/lint/validators/documentation/undocumented_method_arguments.rb +26 -1
  20. data/lib/yard/lint/validators/documentation/undocumented_objects/config.rb +2 -1
  21. data/lib/yard/lint/validators/documentation/undocumented_objects/parser.rb +95 -5
  22. data/lib/yard/lint/validators/documentation/undocumented_objects/validator.rb +15 -11
  23. data/lib/yard/lint/validators/documentation/undocumented_objects.rb +131 -2
  24. data/lib/yard/lint/validators/documentation/undocumented_options/config.rb +20 -0
  25. data/lib/yard/lint/validators/documentation/undocumented_options/parser.rb +53 -0
  26. data/lib/yard/lint/validators/documentation/undocumented_options/result.rb +29 -0
  27. data/lib/yard/lint/validators/documentation/undocumented_options/validator.rb +38 -0
  28. data/lib/yard/lint/validators/documentation/undocumented_options.rb +40 -0
  29. data/lib/yard/lint/validators/semantic/abstract_methods/validator.rb +4 -5
  30. data/lib/yard/lint/validators/semantic/abstract_methods.rb +31 -1
  31. data/lib/yard/lint/validators/tags/api_tags/validator.rb +4 -5
  32. data/lib/yard/lint/validators/tags/api_tags.rb +34 -1
  33. data/lib/yard/lint/validators/tags/collection_type/config.rb +22 -0
  34. data/lib/yard/lint/validators/tags/collection_type/messages_builder.rb +73 -0
  35. data/lib/yard/lint/validators/tags/collection_type/parser.rb +50 -0
  36. data/lib/yard/lint/validators/tags/collection_type/result.rb +25 -0
  37. data/lib/yard/lint/validators/tags/collection_type/validator.rb +92 -0
  38. data/lib/yard/lint/validators/tags/collection_type.rb +50 -0
  39. data/lib/yard/lint/validators/tags/example_syntax/config.rb +20 -0
  40. data/lib/yard/lint/validators/tags/example_syntax/messages_builder.rb +28 -0
  41. data/lib/yard/lint/validators/tags/example_syntax/parser.rb +79 -0
  42. data/lib/yard/lint/validators/tags/example_syntax/result.rb +42 -0
  43. data/lib/yard/lint/validators/tags/example_syntax/validator.rb +88 -0
  44. data/lib/yard/lint/validators/tags/example_syntax.rb +42 -0
  45. data/lib/yard/lint/validators/tags/invalid_types/validator.rb +8 -8
  46. data/lib/yard/lint/validators/tags/invalid_types.rb +25 -1
  47. data/lib/yard/lint/validators/tags/meaningless_tag/config.rb +22 -0
  48. data/lib/yard/lint/validators/tags/meaningless_tag/messages_builder.rb +28 -0
  49. data/lib/yard/lint/validators/tags/meaningless_tag/parser.rb +53 -0
  50. data/lib/yard/lint/validators/tags/meaningless_tag/result.rb +26 -0
  51. data/lib/yard/lint/validators/tags/meaningless_tag/validator.rb +82 -0
  52. data/lib/yard/lint/validators/tags/meaningless_tag.rb +43 -0
  53. data/lib/yard/lint/validators/tags/option_tags/validator.rb +11 -6
  54. data/lib/yard/lint/validators/tags/option_tags.rb +26 -1
  55. data/lib/yard/lint/validators/tags/order/validator.rb +4 -5
  56. data/lib/yard/lint/validators/tags/order.rb +25 -1
  57. data/lib/yard/lint/validators/tags/redundant_param_description/config.rb +33 -0
  58. data/lib/yard/lint/validators/tags/redundant_param_description/messages_builder.rb +61 -0
  59. data/lib/yard/lint/validators/tags/redundant_param_description/parser.rb +67 -0
  60. data/lib/yard/lint/validators/tags/redundant_param_description/result.rb +25 -0
  61. data/lib/yard/lint/validators/tags/redundant_param_description/validator.rb +148 -0
  62. data/lib/yard/lint/validators/tags/redundant_param_description.rb +168 -0
  63. data/lib/yard/lint/validators/tags/tag_type_position/config.rb +22 -0
  64. data/lib/yard/lint/validators/tags/tag_type_position/messages_builder.rb +38 -0
  65. data/lib/yard/lint/validators/tags/tag_type_position/parser.rb +51 -0
  66. data/lib/yard/lint/validators/tags/tag_type_position/result.rb +25 -0
  67. data/lib/yard/lint/validators/tags/tag_type_position/validator.rb +111 -0
  68. data/lib/yard/lint/validators/tags/tag_type_position.rb +51 -0
  69. data/lib/yard/lint/validators/tags/type_syntax/config.rb +21 -0
  70. data/lib/yard/lint/validators/tags/type_syntax/messages_builder.rb +27 -0
  71. data/lib/yard/lint/validators/tags/type_syntax/parser.rb +54 -0
  72. data/lib/yard/lint/validators/tags/type_syntax/result.rb +25 -0
  73. data/lib/yard/lint/validators/tags/type_syntax/validator.rb +76 -0
  74. data/lib/yard/lint/validators/tags/type_syntax.rb +38 -0
  75. data/lib/yard/lint/validators/warnings/duplicated_parameter_name/validator.rb +4 -5
  76. data/lib/yard/lint/validators/warnings/duplicated_parameter_name.rb +26 -1
  77. data/lib/yard/lint/validators/warnings/invalid_directive_format/validator.rb +4 -5
  78. data/lib/yard/lint/validators/warnings/invalid_directive_format.rb +26 -1
  79. data/lib/yard/lint/validators/warnings/invalid_tag_format/validator.rb +4 -5
  80. data/lib/yard/lint/validators/warnings/invalid_tag_format.rb +25 -1
  81. data/lib/yard/lint/validators/warnings/unknown_directive/validator.rb +4 -5
  82. data/lib/yard/lint/validators/warnings/unknown_directive.rb +26 -1
  83. data/lib/yard/lint/validators/warnings/unknown_parameter_name/parser.rb +4 -1
  84. data/lib/yard/lint/validators/warnings/unknown_parameter_name/validator.rb +4 -5
  85. data/lib/yard/lint/validators/warnings/unknown_parameter_name.rb +23 -1
  86. data/lib/yard/lint/validators/warnings/unknown_tag/validator.rb +4 -5
  87. data/lib/yard/lint/validators/warnings/unknown_tag.rb +26 -1
  88. data/lib/yard/lint/version.rb +1 -1
  89. data/lib/yard/lint.rb +1 -0
  90. data/misc/logo.png +0 -0
  91. metadata +64 -1
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ # Tags validators - validate YARD tag quality and consistency
7
+ module Tags
8
+ # RedundantParamDescription validator
9
+ #
10
+ # Detects parameter descriptions that add no meaningful information beyond
11
+ # what's already obvious from the parameter name and type. This validator
12
+ # helps maintain high-quality documentation by flagging descriptions that
13
+ # should either be expanded with meaningful details or removed entirely.
14
+ #
15
+ # ## Why This Matters
16
+ #
17
+ # Documentation is most valuable when AI assistants and human developers
18
+ # can trust it. Redundant descriptions like `@param user [User] The user`
19
+ # create noise without adding value, training both humans and AI to ignore
20
+ # parameter documentation. Better to have no description (letting the type
21
+ # speak for itself) than a redundant one.
22
+ #
23
+ # @example Redundant - Article + param name (will be flagged)
24
+ # # @param appointment [Appointment] The appointment
25
+ # # @param user [User] the user
26
+ # def process(appointment, user)
27
+ # end
28
+ #
29
+ # @example Redundant - Possessive form (will be flagged)
30
+ # # @param appointment [Appointment] The event's appointment
31
+ # def schedule(appointment)
32
+ # end
33
+ #
34
+ # @example Redundant - Type restatement (will be flagged)
35
+ # # @param user [User] User object
36
+ # # @param value [Integer] Integer value
37
+ # def update(user, value)
38
+ # end
39
+ #
40
+ # @example Redundant - Parameter to verb pattern (will be flagged)
41
+ # # @param payments [Array] Payments to count
42
+ # # @param user [User] User to notify
43
+ # def process(payments, user)
44
+ # end
45
+ #
46
+ # @example Redundant - ID pattern (will be flagged)
47
+ # # @param treatment_id [String] ID of the treatment
48
+ # # @param uuid [String] Unique identifier for the list
49
+ # def find(treatment_id, uuid)
50
+ # end
51
+ #
52
+ # @example Redundant - Directional date (will be flagged)
53
+ # # @param from [Date] from this date
54
+ # # @param till [Date] till this date
55
+ # def filter(from, till)
56
+ # end
57
+ #
58
+ # @example Redundant - Type + generic term (will be flagged)
59
+ # # @param payment [Payment] Payment object
60
+ # # @param data [Hash] Hash data
61
+ # def process(payment, data)
62
+ # end
63
+ #
64
+ # @example Good - No description (type is self-explanatory)
65
+ # # @param appointment [Appointment]
66
+ # # @param user [User]
67
+ # def process(appointment, user)
68
+ # end
69
+ #
70
+ # @example Good - Long, meaningful descriptions with context
71
+ # # @param date [Date, nil] the date that can describe the event starting information or nil if event did not yet start
72
+ # # @param user [User] the user who initiated the request and will receive notifications
73
+ # # @param data [Hash] configuration options for the API endpoint including timeout and retry settings
74
+ # def configure(date, user, data)
75
+ # end
76
+ #
77
+ # @example Good - Short but adds value beyond param name
78
+ # # @param count [Integer] maximum number of retries before giving up
79
+ # # @param timeout [Integer] seconds to wait before timing out the connection
80
+ # # @param enabled [Boolean] whether the feature is enabled for this account
81
+ # def setup(count, timeout, enabled)
82
+ # end
83
+ #
84
+ # @example Good - Starts similar but continues with valuable info
85
+ # # @param user [User] the current user, or guest if not authenticated
86
+ # # @param data [Hash] the request payload containing user preferences
87
+ # # @param id [String] unique identifier used for tracking across systems
88
+ # def track(user, data, id)
89
+ # end
90
+ #
91
+ # ## Configuration
92
+ #
93
+ # The validator is highly configurable to match your project's needs:
94
+ #
95
+ # Tags/RedundantParamDescription:
96
+ # Description: 'Detects meaningless parameter descriptions.'
97
+ # Enabled: true
98
+ # Severity: convention
99
+ # CheckedTags:
100
+ # - param
101
+ # - option
102
+ # # Articles that trigger the article_param pattern
103
+ # Articles:
104
+ # - The
105
+ # - the
106
+ # - A
107
+ # - a
108
+ # - An
109
+ # - an
110
+ # # Maximum word count for redundant descriptions (longer descriptions are never flagged)
111
+ # MaxRedundantWords: 6
112
+ # # Generic terms that trigger the type_generic pattern
113
+ # GenericTerms:
114
+ # - object
115
+ # - instance
116
+ # - value
117
+ # - data
118
+ # - item
119
+ # - element
120
+ # # Pattern toggles (enable/disable specific detection patterns)
121
+ # EnabledPatterns:
122
+ # ArticleParam: true # "The user", "the appointment"
123
+ # PossessiveParam: true # "The event's appointment"
124
+ # TypeRestatement: true # "User object", "Appointment"
125
+ # ParamToVerb: true # "Payments to count"
126
+ # IdPattern: true # "ID of the treatment" for *_id params
127
+ # DirectionalDate: true # "from this date" for from/to/till
128
+ # TypeGeneric: true # "Payment object", "Hash data"
129
+ #
130
+ # ## Pattern Types
131
+ #
132
+ # The validator detects 7 different redundancy patterns:
133
+ #
134
+ # 1. **ArticleParam**: `"The user"`, `"the appointment"` - Article + parameter name
135
+ # 2. **PossessiveParam**: `"The event's appointment"` - Possessive form + parameter name
136
+ # 3. **TypeRestatement**: `"User object"`, `"Appointment"` - Just repeats the type
137
+ # 4. **ParamToVerb**: `"Payments to count"` - Parameter name + "to" + verb
138
+ # 5. **IdPattern**: `"ID of the treatment"` - For `_id` or `_uuid` suffixed parameters
139
+ # 6. **DirectionalDate**: `"from this date"` - For `from`, `to`, `till` parameters
140
+ # 7. **TypeGeneric**: `"Payment object"` - Type + generic term like "object", "instance"
141
+ #
142
+ # You can disable individual patterns while keeping others enabled.
143
+ #
144
+ # ## False Positive Prevention
145
+ #
146
+ # The validator uses multiple strategies to prevent false positives:
147
+ #
148
+ # 1. **Word count threshold**: Descriptions longer than `MaxRedundantWords` (default: 6)
149
+ # are never flagged, even if they start with a redundant pattern
150
+ # 2. **EXACT pattern matching**: Only flags complete matches, not partial/prefix matches
151
+ # 3. **Configurable patterns**: Disable patterns that don't work for your codebase
152
+ #
153
+ # This means `"the date that can describe the event starting information"` (9 words)
154
+ # will never be flagged, even though it starts with "the date".
155
+ #
156
+ # ## Disabling
157
+ #
158
+ # To disable this validator:
159
+ #
160
+ # Tags/RedundantParamDescription:
161
+ # Enabled: false
162
+ #
163
+ module RedundantParamDescription
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TagTypePosition
8
+ # Configuration for TagTypePosition validator
9
+ class Config < ::Yard::Lint::Validators::Config
10
+ self.id = :tag_type_position
11
+ self.defaults = {
12
+ 'Enabled' => true,
13
+ 'Severity' => 'convention',
14
+ 'CheckedTags' => %w[param option],
15
+ 'EnforcedStyle' => 'type_after_name' # 'type_after_name' (standard) or 'type_first'
16
+ }.freeze
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TagTypePosition
8
+ # Builds human-readable messages for TagTypePosition violations
9
+ class MessagesBuilder
10
+ class << self
11
+ # Formats a violation message
12
+ # @param offense [Hash] the offense details
13
+ # @return [String] formatted message
14
+ def call(offense)
15
+ tag_name = offense[:tag_name]
16
+ param_name = offense[:param_name]
17
+ type_info = offense[:type_info]
18
+ detected_style = offense[:detected_style]
19
+
20
+ if detected_style == 'type_after_name'
21
+ # Enforcing type_first, but found type_after_name
22
+ "Type should appear before parameter name in @#{tag_name} tag. " \
23
+ "Use '@#{tag_name} [#{type_info}] #{param_name}' instead of " \
24
+ "'@#{tag_name} #{param_name} [#{type_info}]'."
25
+ else
26
+ # Enforcing type_after_name, but found type_first
27
+ "Type should appear after parameter name in @#{tag_name} tag. " \
28
+ "Use '@#{tag_name} #{param_name} [#{type_info}]' instead of " \
29
+ "'@#{tag_name} [#{type_info}] #{param_name}'."
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TagTypePosition
8
+ # Parses YARD output for TagTypePosition violations
9
+ class Parser < ::Yard::Lint::Parsers::Base
10
+ # Parses YARD query output into structured violation data
11
+ # @param yard_output [String] raw output from YARD query
12
+ # @param _kwargs [Hash] additional keyword arguments (unused)
13
+ # @return [Array<Hash>] array of violation hashes
14
+ def call(yard_output, **_kwargs)
15
+ return [] if yard_output.nil? || yard_output.strip.empty?
16
+
17
+ lines = yard_output.split("\n").map(&:strip).reject(&:empty?)
18
+ violations = []
19
+
20
+ lines.each_slice(2) do |location_line, details_line|
21
+ next unless location_line && details_line
22
+
23
+ # Parse location: "file.rb:10: ClassName#method_name"
24
+ location_match = location_line.match(/^(.+):(\d+): (.+)$/)
25
+ next unless location_match
26
+
27
+ # Parse details: "tag_name|param_name|type_info|detected_style"
28
+ details = details_line.split('|', 4)
29
+ next unless details.size >= 3
30
+
31
+ tag_name, param_name, type_info, detected_style = details
32
+
33
+ violations << {
34
+ location: location_match[1],
35
+ line: location_match[2].to_i,
36
+ object_name: location_match[3],
37
+ tag_name: tag_name,
38
+ param_name: param_name,
39
+ type_info: type_info,
40
+ detected_style: detected_style
41
+ }
42
+ end
43
+
44
+ violations
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TagTypePosition
8
+ # Result wrapper for TagTypePosition violations
9
+ class Result < Results::Base
10
+ self.default_severity = 'convention'
11
+ self.offense_type = 'style'
12
+ self.offense_name = 'TagTypePosition'
13
+
14
+ # Builds a human-readable message for a violation
15
+ # @param offense [Hash] the offense 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,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TagTypePosition
8
+ # Validates type annotation position in @param and @option tags
9
+ # YARD standard (type_after_name): @param name [String] description
10
+ # Alternative (type_first): @param name [String] description
11
+ #
12
+ # Note: @return tags are not checked as they don't have parameter names
13
+ class Validator < Base
14
+ private
15
+
16
+ # Runs YARD query to check type position in tags
17
+ # @param dir [String] directory where YARD database is stored
18
+ # @param file_list_path [String] path to temp file containing file paths (one per line)
19
+ # @return [Hash] shell command execution results
20
+ def yard_cmd(dir, file_list_path)
21
+ # Write query to a temporary file to avoid shell escaping issues
22
+ cmd = "cat #{Shellwords.escape(file_list_path)} | xargs yard list --query #{query} "
23
+
24
+ Tempfile.create(['yard_query', '.sh']) do |f|
25
+ f.write("#!/bin/bash\n")
26
+ f.write(cmd)
27
+ f.write("#{shell_arguments} -b #{Shellwords.escape(dir)}\n")
28
+ f.flush
29
+ f.chmod(0o755)
30
+
31
+ shell("bash #{Shellwords.escape(f.path)}")
32
+ end
33
+ end
34
+
35
+ # YARD query that checks source code directly instead of docstring.all
36
+ # Detects patterns based on configured style
37
+ # @return [String] YARD query string
38
+ def query
39
+ <<~QUERY.strip
40
+ '
41
+ require "ripper"
42
+
43
+ checked_tags = #{checked_tags_array}
44
+ enforced_style = "#{enforced_style}"
45
+
46
+ # Read the source file and find comment lines for this object
47
+ return false unless object.file && File.exist?(object.file)
48
+
49
+ source_lines = File.readlines(object.file)
50
+ start_line = [object.line - 50, 0].max
51
+ end_line = [object.line, source_lines.length - 1].min
52
+
53
+ # Look for comments before the object definition
54
+ # Start just before the object line and scan backward
55
+ (start_line...(end_line - 1)).reverse_each do |line_num|
56
+ line = source_lines[line_num].to_s.strip
57
+
58
+ # Skip empty lines
59
+ next if line.empty?
60
+
61
+ # Stop if we hit code (non-comment line)
62
+ break unless line.start_with?("#")
63
+
64
+ # Skip comment-only lines without tags
65
+ next unless line.include?("@")
66
+
67
+ checked_tags.each do |tag_name|
68
+ if enforced_style == "type_first"
69
+ # Detect: @tag_name word [Type] (violation when type_first is enforced)
70
+ pattern = /@\#{tag_name}\\s+(\\w+)\\s+\\[([^\\]]+)\\]/
71
+ if line =~ pattern
72
+ param_name = $1
73
+ type_info = $2
74
+ puts object.file + ":" + (line_num + 1).to_s + ": " + object.title
75
+ puts tag_name + "|" + param_name + "|" + type_info + "|type_after_name"
76
+ end
77
+ else
78
+ # Detect: @tag_name [Type] word (violation when type_after_name is enforced)
79
+ pattern = /@\#{tag_name}\\s+\\[([^\\]]+)\\]\\s+(\\w+)/
80
+ if line =~ pattern
81
+ type_info = $1
82
+ param_name = $2
83
+ puts object.file + ":" + (line_num + 1).to_s + ": " + object.title
84
+ puts tag_name + "|" + param_name + "|" + type_info + "|type_first"
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ false
91
+ '
92
+ QUERY
93
+ end
94
+
95
+ # @return [String] the enforced style ('type_after_name' (standard) or 'type_first')
96
+ def enforced_style
97
+ config_or_default('EnforcedStyle')
98
+ end
99
+
100
+ # Array of tag names to check, formatted for YARD query
101
+ # @return [String] Ruby array literal string
102
+ def checked_tags_array
103
+ tags = config_or_default('CheckedTags')
104
+ "[#{tags.map { |t| "\"#{t}\"" }.join(',')}]"
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ # TagTypePosition validator
8
+ #
9
+ # Ensures consistent type annotation positioning in YARD documentation tags.
10
+ # By default, it enforces YARD standard style where the type appears after
11
+ # the parameter name (`@param name [Type]`), but you can configure it to
12
+ # enforce `type_first` style if your team prefers that convention.
13
+ # This validator is enabled by default.
14
+ #
15
+ # @example Good - Type after parameter name (YARD standard)
16
+ # # @param name [String] the user's name
17
+ # # @param age [Integer] the user's age
18
+ # # @param options [Hash{Symbol => Object}] configuration options
19
+ # def create_user(name, age, options = {})
20
+ # end
21
+ #
22
+ # @example Bad - Type before parameter name (when using default style)
23
+ # # @param [String] name the user's name
24
+ # # @param [Integer] age the user's age
25
+ # def create_user(name, age)
26
+ # end
27
+ #
28
+ # ## Configuration
29
+ #
30
+ # To use type_first style instead:
31
+ #
32
+ # Tags/TagTypePosition:
33
+ # EnforcedStyle: type_first
34
+ #
35
+ # @example Good - When EnforcedStyle is type_first
36
+ # # @param [String] name the user's name
37
+ # # @param [Integer] age the user's age
38
+ # def create_user(name, age)
39
+ # end
40
+ #
41
+ # To disable this validator:
42
+ #
43
+ # Tags/TagTypePosition:
44
+ # Enabled: false
45
+ #
46
+ module TagTypePosition
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TypeSyntax
8
+ # Configuration for TypeSyntax validator
9
+ class Config < ::Yard::Lint::Validators::Config
10
+ self.id = :type_syntax
11
+ self.defaults = {
12
+ 'Enabled' => true,
13
+ 'Severity' => 'warning',
14
+ 'ValidatedTags' => %w[param option return yieldreturn]
15
+ }.freeze
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TypeSyntax
8
+ # Builds human-readable messages for type syntax violations
9
+ class MessagesBuilder
10
+ class << self
11
+ # Formats a type syntax violation message
12
+ # @param offense [Hash] offense details with tag_name, type_string, error_message
13
+ # @return [String] formatted message
14
+ def call(offense)
15
+ tag = offense[:tag_name]
16
+ type = offense[:type_string]
17
+ error = offense[:error_message]
18
+
19
+ "Invalid type syntax in @#{tag} tag: '#{type}' (#{error})"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TypeSyntax
8
+ # Parser for TypeSyntax validator output
9
+ # Parses YARD query output that reports type syntax errors
10
+ class Parser < ::Yard::Lint::Parsers::Base
11
+ # Parses YARD output and extracts type syntax violations
12
+ # Expected format (two lines per violation):
13
+ # file.rb:LINE: ClassName#method_name
14
+ # tag_name|type_string|error_message
15
+ # @param yard_output [String] raw YARD query results
16
+ # @param _kwargs [Hash] unused keyword arguments (for compatibility)
17
+ # @return [Array<Hash>] array with violation details
18
+ def call(yard_output, **_kwargs)
19
+ return [] if yard_output.nil? || yard_output.strip.empty?
20
+
21
+ lines = yard_output.split("\n").map(&:strip).reject(&:empty?)
22
+ violations = []
23
+
24
+ lines.each_slice(2) do |location_line, details_line|
25
+ next unless location_line && details_line
26
+
27
+ # Parse location: "file.rb:10: ClassName#method_name"
28
+ location_match = location_line.match(/^(.+):(\d+): (.+)$/)
29
+ next unless location_match
30
+
31
+ # Parse details: "tag_name|type_string|error_message"
32
+ details = details_line.split('|', 3)
33
+ next unless details.size == 3
34
+
35
+ tag_name, type_string, error_message = details
36
+
37
+ violations << {
38
+ location: location_match[1],
39
+ line: location_match[2].to_i,
40
+ method_name: location_match[3],
41
+ tag_name: tag_name,
42
+ type_string: type_string,
43
+ error_message: error_message
44
+ }
45
+ end
46
+
47
+ violations
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TypeSyntax
8
+ # Result wrapper for TypeSyntax validator
9
+ class Result < Results::Base
10
+ self.default_severity = 'warning'
11
+ self.offense_type = 'method'
12
+ self.offense_name = 'InvalidTypeSyntax'
13
+
14
+ # Builds a human-readable message for the offense
15
+ # @param offense [Hash] offense 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,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module TypeSyntax
8
+ # Runs YARD to validate type syntax using TypesExplainer::Parser
9
+ class Validator < Base
10
+ private
11
+
12
+ # Runs YARD query to validate type syntax on given files
13
+ # @param dir [String] directory where YARD database is stored
14
+ # @param file_list_path [String] path to temp file containing file paths (one per line)
15
+ # @return [Hash] shell command execution results
16
+ def yard_cmd(dir, file_list_path)
17
+ # Write query to a temporary file to avoid shell escaping issues
18
+ cmd = "cat #{Shellwords.escape(file_list_path)} | xargs yard list --query #{query} "
19
+
20
+ Tempfile.create(['yard_query', '.sh']) do |f|
21
+ f.write("#!/bin/bash\n")
22
+ f.write(cmd)
23
+ f.write("#{shell_arguments} -b #{Shellwords.escape(dir)}\n")
24
+ f.flush
25
+ f.chmod(0o755)
26
+
27
+ shell("bash #{Shellwords.escape(f.path)}")
28
+ end
29
+ end
30
+
31
+ # YARD query that validates type syntax for each tag
32
+ # Format output as two lines per violation:
33
+ # Line 1: file.rb:LINE: ClassName#method_name
34
+ # Line 2: tag_name|type_string|error_message
35
+ # @return [String] YARD query string
36
+ def query
37
+ <<~QUERY.strip
38
+ '
39
+ require "yard"
40
+
41
+ docstring
42
+ .tags
43
+ .select { |tag| #{validated_tags_array}.include?(tag.tag_name) }
44
+ .each do |tag|
45
+ next unless tag.types
46
+
47
+ tag.types.each do |type_str|
48
+ begin
49
+ YARD::Tags::TypesExplainer::Parser.parse(type_str)
50
+ rescue SyntaxError => e
51
+ puts object.file + ":" + object.line.to_s + ": " + object.title
52
+ puts tag.tag_name + "|" + type_str + "|" + e.message
53
+ break
54
+ end
55
+ end
56
+ end
57
+
58
+ false
59
+ '
60
+ QUERY
61
+ end
62
+
63
+ # Array of tag names to validate, formatted for YARD query
64
+ # @return [String] Ruby array literal string
65
+ def validated_tags_array
66
+ tags = config.validator_config('Tags/TypeSyntax', 'ValidatedTags') || %w[
67
+ param option return yieldreturn
68
+ ]
69
+ "[#{tags.map { |t| "\"#{t}\"" }.join(',')}]"
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end