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,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module MeaninglessTag
8
+ # Validates that @param/@option tags only appear on methods
9
+ class Validator < Base
10
+ private
11
+
12
+ # Runs YARD query to find @param/@option tags on non-methods
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("--private --protected #{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 finds method-only tags on non-method objects
32
+ # Format output as two lines per violation:
33
+ # Line 1: file.rb:LINE: ClassName
34
+ # Line 2: object_type|tag_name
35
+ # @return [String] YARD query string
36
+ def query
37
+ <<~QUERY.strip
38
+ '
39
+ object_type = object.type.to_s
40
+
41
+ if #{invalid_object_types_array}.include?(object_type)
42
+ docstring.tags.each do |tag|
43
+ if #{checked_tags_array}.include?(tag.tag_name)
44
+ puts object.file + ":" + object.line.to_s + ": " + object.title
45
+ puts object_type + "|" + tag.tag_name
46
+ break
47
+ end
48
+ end
49
+ end
50
+
51
+ false
52
+ '
53
+ QUERY
54
+ end
55
+
56
+ # Array of tag names to check, formatted for YARD query
57
+ # @return [String] Ruby array literal string
58
+ def checked_tags_array
59
+ "[#{checked_tags.map { |t| "\"#{t}\"" }.join(',')}]"
60
+ end
61
+
62
+ # Array of invalid object types, formatted for YARD query
63
+ # @return [String] Ruby array literal string
64
+ def invalid_object_types_array
65
+ "[#{invalid_object_types.map { |t| "\"#{t}\"" }.join(',')}]"
66
+ end
67
+
68
+ # @return [Array<String>] tags that should only appear on methods
69
+ def checked_tags
70
+ config_or_default('CheckedTags')
71
+ end
72
+
73
+ # @return [Array<String>] object types that shouldn't have method-only tags
74
+ def invalid_object_types
75
+ config_or_default('InvalidObjectTypes')
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ # MeaninglessTag validator
8
+ #
9
+ # Prevents `@param` and `@option` tags from being used on classes, modules,
10
+ # or constants, where they make no sense. These tags are only valid on methods.
11
+ # This validator is enabled by default.
12
+ #
13
+ # ## Configuration
14
+ #
15
+ # To disable this validator:
16
+ #
17
+ # Tags/MeaninglessTag:
18
+ # Enabled: false
19
+ #
20
+ # @example Bad - @param on a class
21
+ # # @param name [String] this makes no sense on a class
22
+ # class User
23
+ # end
24
+ #
25
+ # @example Bad - @option on a module
26
+ # # @option config [Boolean] :enabled modules don't have parameters
27
+ # module Authentication
28
+ # end
29
+ #
30
+ # @example Good - @param on a method
31
+ # class User
32
+ # # @param name [String] the user's name
33
+ # def initialize(name)
34
+ # @name = name
35
+ # end
36
+ # end
37
+ #
38
+ module MeaninglessTag
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -11,15 +11,14 @@ module Yard
11
11
 
12
12
  # Runs yard list query to find methods with options parameter but missing @option tags
13
13
  # @param dir [String] dir where the yard db is (or where it should be generated)
14
- # @param escaped_file_names [String] files for which we want to get the stats
14
+ # @param file_list_path [String] path to temp file containing file paths (one per line)
15
15
  # @return [Hash] shell command execution hash results
16
- def yard_cmd(dir, escaped_file_names)
16
+ def yard_cmd(dir, file_list_path)
17
17
  cmd = <<~CMD
18
- yard list \
18
+ cat #{Shellwords.escape(file_list_path)} | xargs yard list \
19
19
  --private \
20
20
  --protected \
21
- -b #{Shellwords.escape(dir)} \
22
- #{escaped_file_names}
21
+ -b #{Shellwords.escape(dir)}
23
22
  CMD
24
23
  cmd = cmd.tr("\n", ' ')
25
24
  cmd = cmd.gsub('yard list', "yard list --query #{query}")
@@ -30,13 +29,14 @@ module Yard
30
29
  # @return [String] yard query to find methods with options parameter
31
30
  # but no @option tags
32
31
  def query
32
+ parameter_names = config_parameter_names
33
33
  <<~QUERY
34
34
  '
35
35
  if object.is_a?(YARD::CodeObjects::MethodObject)
36
36
  # Check if method has a parameter named "options" or "opts"
37
37
  has_options_param = object.parameters.any? do |param|
38
38
  param_name = param[0].to_s.gsub(/[*:]/, '')
39
- ['options', 'opts', 'kwargs'].include?(param_name)
39
+ #{parameter_names.inspect}.include?(param_name)
40
40
  end
41
41
 
42
42
  if has_options_param
@@ -53,6 +53,11 @@ module Yard
53
53
  '
54
54
  QUERY
55
55
  end
56
+
57
+ # @return [Array<String>] parameter names that should have @option tags
58
+ def config_parameter_names
59
+ config.validator_config('Tags/OptionTags', 'ParameterNames') || Config.defaults['ParameterNames']
60
+ end
56
61
  end
57
62
  end
58
63
  end
@@ -4,7 +4,32 @@ module Yard
4
4
  module Lint
5
5
  module Validators
6
6
  module Tags
7
- # OptionTags validator module
7
+ # OptionTags validator
8
+ #
9
+ # Ensures that methods with options parameters document them using `@option`
10
+ # tags. This validator is enabled by default.
11
+ #
12
+ # ## Configuration
13
+ #
14
+ # To disable this validator:
15
+ #
16
+ # Tags/OptionTags:
17
+ # Enabled: false
18
+ #
19
+ # @example Good - Options hash is documented
20
+ # # @param name [String] the name
21
+ # # @param options [Hash] configuration options
22
+ # # @option options [Boolean] :enabled Whether to enable the feature
23
+ # # @option options [Integer] :timeout Timeout in seconds
24
+ # def configure(name, options = {})
25
+ # end
26
+ #
27
+ # @example Bad - Missing @option tags
28
+ # # @param name [String] the name
29
+ # # @param options [Hash] configuration options
30
+ # def configure(name, options = {})
31
+ # end
32
+ #
8
33
  module OptionTags
9
34
  end
10
35
  end
@@ -12,16 +12,15 @@ module Yard
12
12
 
13
13
  # Runs yard list query with proper settings on a given dir and files
14
14
  # @param dir [String] dir where the yard db is (or where it should be generated)
15
- # @param escaped_file_names [String] files for which we want to get the stats
15
+ # @param file_list_path [String] path to temp file containing file paths (one per line)
16
16
  # @return [Hash] shell command execution hash results
17
- def yard_cmd(dir, escaped_file_names)
17
+ def yard_cmd(dir, file_list_path)
18
18
  cmd = <<~CMD
19
- yard list \
19
+ cat #{Shellwords.escape(file_list_path)} | xargs yard list \
20
20
  --private \
21
21
  --protected \
22
22
  --query #{query} \
23
- -b #{Shellwords.escape(dir)} \
24
- #{escaped_file_names}
23
+ -b #{Shellwords.escape(dir)}
25
24
  CMD
26
25
 
27
26
  result = shell(cmd)
@@ -4,7 +4,31 @@ module Yard
4
4
  module Lint
5
5
  module Validators
6
6
  module Tags
7
- # Order validator module
7
+ # Order validator
8
+ #
9
+ # Enforces a consistent order for YARD documentation tags. This validator
10
+ # checks that tags appear in a logical sequence (e.g., `@param` before
11
+ # `@return`, `@option` after `@param`). This validator is enabled by default.
12
+ #
13
+ # @example Bad - Tags in wrong order
14
+ # # @return [String] the result
15
+ # # @param name [String] the name
16
+ # def process(name)
17
+ # end
18
+ #
19
+ # @example Good - Tags in correct order
20
+ # # @param name [String] the name
21
+ # # @return [String] the result
22
+ # def process(name)
23
+ # end
24
+ #
25
+ # ## Configuration
26
+ #
27
+ # To disable this validator:
28
+ #
29
+ # Tags/Order:
30
+ # Enabled: false
31
+ #
8
32
  module Order
9
33
  end
10
34
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module RedundantParamDescription
8
+ # Configuration for RedundantParamDescription validator
9
+ class Config < ::Yard::Lint::Validators::Config
10
+ self.id = :redundant_param_description
11
+ self.defaults = {
12
+ 'Enabled' => true,
13
+ 'Severity' => 'convention',
14
+ 'CheckedTags' => %w[param option],
15
+ 'Articles' => %w[The the A a An an],
16
+ 'MaxRedundantWords' => 6,
17
+ 'GenericTerms' => %w[object instance value data item element],
18
+ 'EnabledPatterns' => {
19
+ 'ArticleParam' => true,
20
+ 'PossessiveParam' => true,
21
+ 'TypeRestatement' => true,
22
+ 'ParamToVerb' => true,
23
+ 'IdPattern' => true,
24
+ 'DirectionalDate' => true,
25
+ 'TypeGeneric' => true
26
+ }
27
+ }.freeze
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module RedundantParamDescription
8
+ # Builds human-readable messages for redundant parameter description violations
9
+ class MessagesBuilder
10
+ # Build message for a violation
11
+ # @param offense [Hash] the offense data
12
+ # @return [String] formatted message
13
+ def self.call(offense)
14
+ tag_name = offense[:tag_name]
15
+ param_name = offense[:param_name]
16
+ description = offense[:description]
17
+ pattern_type = offense[:pattern_type]
18
+
19
+ case pattern_type
20
+ when 'article_param'
21
+ 'The @' + tag_name + ' description \'' + description + '\' is redundant - ' \
22
+ 'it just restates the parameter name. ' \
23
+ 'Consider removing the description: @' + tag_name + ' ' + param_name + ' [Type]'
24
+
25
+ when 'possessive_param'
26
+ 'The @' + tag_name + ' description \'' + description + '\' adds no meaningful information ' \
27
+ 'beyond the parameter name. ' \
28
+ 'Consider removing it or explaining the parameter\'s specific purpose.'
29
+
30
+ when 'type_restatement'
31
+ 'The @' + tag_name + ' description \'' + description + '\' just repeats the type name. ' \
32
+ 'Consider removing the description or explaining what makes this ' + param_name + ' significant.'
33
+
34
+ when 'param_to_verb'
35
+ 'The @' + tag_name + ' description \'' + description + '\' is too generic. ' \
36
+ 'Consider removing it or explaining what the ' + param_name + ' is used for in detail.'
37
+
38
+ when 'id_pattern'
39
+ 'The @' + tag_name + ' description \'' + description + '\' is self-explanatory from the parameter name. ' \
40
+ 'Consider removing the description: @' + tag_name + ' ' + param_name + ' [Type]'
41
+
42
+ when 'directional_date'
43
+ 'The @' + tag_name + ' description \'' + description + '\' is redundant - ' \
44
+ 'the parameter name already indicates this. ' \
45
+ 'Consider removing the description or explaining the date\'s specific meaning.'
46
+
47
+ when 'type_generic'
48
+ 'The @' + tag_name + ' description \'' + description + '\' just combines type and generic terms. ' \
49
+ 'Consider removing it or providing specific details about this ' + param_name + '.'
50
+
51
+ else
52
+ 'The @' + tag_name + ' description \'' + description + '\' appears redundant. ' \
53
+ 'Consider providing a meaningful description or omitting it entirely.'
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ 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 RedundantParamDescription
8
+ # Parses YARD output for redundant parameter description violations
9
+ class Parser < Parsers::Base
10
+ # Parse YARD output into structured violations
11
+ # @param yard_output [String] raw YARD output
12
+ # @return [Array<Hash>] array of violation hashes
13
+ def call(yard_output)
14
+ return [] if yard_output.nil? || yard_output.empty?
15
+
16
+ violations = []
17
+ lines = yard_output.lines.map(&:chomp)
18
+
19
+ i = 0
20
+ while i < lines.length
21
+ line = lines[i]
22
+
23
+ # Match location line: "file:line: object_name"
24
+ location_match = line.match(/^(.+):(\d+): (.+)$/)
25
+ unless location_match
26
+ i += 1
27
+ next
28
+ end
29
+
30
+ file_path = location_match[1]
31
+ line_number = location_match[2].to_i
32
+ object_name = location_match[3]
33
+
34
+ # Next line contains violation data
35
+ i += 1
36
+ next unless i < lines.length
37
+
38
+ data_line = lines[i]
39
+ parts = data_line.split('|')
40
+ next unless parts.length == 6
41
+
42
+ tag_name, param_name, description, type_name, pattern_type, word_count = parts
43
+
44
+ violations << {
45
+ name: 'RedundantParamDescription',
46
+ tag_name: tag_name,
47
+ param_name: param_name,
48
+ description: description,
49
+ type_name: type_name.empty? ? nil : type_name,
50
+ pattern_type: pattern_type,
51
+ word_count: word_count.to_i,
52
+ location: file_path,
53
+ line: line_number,
54
+ object_name: object_name
55
+ }
56
+
57
+ i += 1
58
+ end
59
+
60
+ violations
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ 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 RedundantParamDescription
8
+ # Result builder for redundant parameter description violations
9
+ class Result < Results::Base
10
+ self.default_severity = 'convention'
11
+ self.offense_type = 'tag'
12
+ self.offense_name = 'RedundantParamDescription'
13
+
14
+ # Build human-readable message for redundant param offense
15
+ # @param offense [Hash] offense data with redundancy 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,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module RedundantParamDescription
8
+ # Validates that parameter descriptions are not redundant/meaningless
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('|')
15
+ max_words = config_max_redundant_words
16
+ checked_tags = config_checked_tags
17
+ patterns = config_enabled_patterns
18
+
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}'"
94
+ end
95
+
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
115
+ end
116
+
117
+ private
118
+
119
+ # @return [Array<String>] configured articles to check
120
+ def config_articles
121
+ config_or_default('Articles')
122
+ end
123
+
124
+ # @return [Array<String>] configured generic terms to check
125
+ def config_generic_terms
126
+ config_or_default('GenericTerms')
127
+ end
128
+
129
+ # @return [Integer] maximum word count for redundant descriptions
130
+ def config_max_redundant_words
131
+ config_or_default('MaxRedundantWords')
132
+ end
133
+
134
+ # @return [Array<String>] tags to check for redundant descriptions
135
+ def config_checked_tags
136
+ config_or_default('CheckedTags')
137
+ end
138
+
139
+ # @return [Hash] enabled pattern detection flags
140
+ def config_enabled_patterns
141
+ config_or_default('EnabledPatterns')
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end