yard-lint 1.0.0 → 1.2.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/README.md +160 -268
  4. data/bin/yard-lint +100 -8
  5. data/lib/yard/lint/command_cache.rb +17 -1
  6. data/lib/yard/lint/config.rb +20 -2
  7. data/lib/yard/lint/config_generator.rb +200 -0
  8. data/lib/yard/lint/ext/irb_notifier_shim.rb +95 -0
  9. data/lib/yard/lint/git.rb +125 -0
  10. data/lib/yard/lint/results/aggregate.rb +22 -2
  11. data/lib/yard/lint/runner.rb +4 -3
  12. data/lib/yard/lint/stats_calculator.rb +157 -0
  13. data/lib/yard/lint/validators/base.rb +36 -0
  14. data/lib/yard/lint/validators/documentation/markdown_syntax/config.rb +20 -0
  15. data/lib/yard/lint/validators/documentation/markdown_syntax/messages_builder.rb +44 -0
  16. data/lib/yard/lint/validators/documentation/markdown_syntax/parser.rb +53 -0
  17. data/lib/yard/lint/validators/documentation/markdown_syntax/result.rb +25 -0
  18. data/lib/yard/lint/validators/documentation/markdown_syntax/validator.rb +38 -0
  19. data/lib/yard/lint/validators/documentation/markdown_syntax.rb +37 -0
  20. data/lib/yard/lint/validators/documentation/undocumented_boolean_methods.rb +26 -1
  21. data/lib/yard/lint/validators/documentation/undocumented_method_arguments.rb +26 -1
  22. data/lib/yard/lint/validators/documentation/undocumented_objects.rb +131 -2
  23. data/lib/yard/lint/validators/documentation/undocumented_options/config.rb +20 -0
  24. data/lib/yard/lint/validators/documentation/undocumented_options/parser.rb +53 -0
  25. data/lib/yard/lint/validators/documentation/undocumented_options/result.rb +29 -0
  26. data/lib/yard/lint/validators/documentation/undocumented_options/validator.rb +38 -0
  27. data/lib/yard/lint/validators/documentation/undocumented_options.rb +40 -0
  28. data/lib/yard/lint/validators/semantic/abstract_methods.rb +31 -1
  29. data/lib/yard/lint/validators/tags/api_tags.rb +34 -1
  30. data/lib/yard/lint/validators/tags/collection_type/config.rb +2 -1
  31. data/lib/yard/lint/validators/tags/collection_type/messages_builder.rb +40 -11
  32. data/lib/yard/lint/validators/tags/collection_type/parser.rb +6 -5
  33. data/lib/yard/lint/validators/tags/collection_type/validator.rb +26 -7
  34. data/lib/yard/lint/validators/tags/collection_type.rb +38 -2
  35. data/lib/yard/lint/validators/tags/example_syntax/config.rb +20 -0
  36. data/lib/yard/lint/validators/tags/example_syntax/messages_builder.rb +28 -0
  37. data/lib/yard/lint/validators/tags/example_syntax/parser.rb +79 -0
  38. data/lib/yard/lint/validators/tags/example_syntax/result.rb +42 -0
  39. data/lib/yard/lint/validators/tags/example_syntax/validator.rb +88 -0
  40. data/lib/yard/lint/validators/tags/example_syntax.rb +42 -0
  41. data/lib/yard/lint/validators/tags/invalid_types/validator.rb +2 -2
  42. data/lib/yard/lint/validators/tags/invalid_types.rb +25 -1
  43. data/lib/yard/lint/validators/tags/meaningless_tag/validator.rb +2 -4
  44. data/lib/yard/lint/validators/tags/meaningless_tag.rb +31 -3
  45. data/lib/yard/lint/validators/tags/option_tags/validator.rb +7 -1
  46. data/lib/yard/lint/validators/tags/option_tags.rb +26 -1
  47. data/lib/yard/lint/validators/tags/order.rb +25 -1
  48. data/lib/yard/lint/validators/tags/redundant_param_description/config.rb +33 -0
  49. data/lib/yard/lint/validators/tags/redundant_param_description/messages_builder.rb +61 -0
  50. data/lib/yard/lint/validators/tags/redundant_param_description/parser.rb +67 -0
  51. data/lib/yard/lint/validators/tags/redundant_param_description/result.rb +25 -0
  52. data/lib/yard/lint/validators/tags/redundant_param_description/validator.rb +148 -0
  53. data/lib/yard/lint/validators/tags/redundant_param_description.rb +168 -0
  54. data/lib/yard/lint/validators/tags/tag_type_position/validator.rb +2 -4
  55. data/lib/yard/lint/validators/tags/tag_type_position.rb +39 -2
  56. data/lib/yard/lint/validators/tags/type_syntax.rb +26 -2
  57. data/lib/yard/lint/validators/warnings/duplicated_parameter_name.rb +26 -1
  58. data/lib/yard/lint/validators/warnings/invalid_directive_format.rb +26 -1
  59. data/lib/yard/lint/validators/warnings/invalid_tag_format.rb +25 -1
  60. data/lib/yard/lint/validators/warnings/unknown_directive.rb +26 -1
  61. data/lib/yard/lint/validators/warnings/unknown_parameter_name.rb +23 -1
  62. data/lib/yard/lint/validators/warnings/unknown_tag.rb +26 -1
  63. data/lib/yard/lint/version.rb +1 -1
  64. data/lib/yard/lint.rb +38 -2
  65. data/lib/yard-lint.rb +5 -0
  66. metadata +28 -1
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Tags
7
+ module ExampleSyntax
8
+ # Validator to check syntax of code in @example tags
9
+ class Validator < Base
10
+ private
11
+
12
+ # Runs yard list query to find objects with invalid syntax in @example tags
13
+ # @param dir [String] dir where the yard db is (or where it should be generated)
14
+ # @param file_list_path [String] path to temp file containing file paths (one per line)
15
+ # @return [Hash] shell command execution hash results
16
+ def yard_cmd(dir, file_list_path)
17
+ cmd = <<~CMD
18
+ cat #{Shellwords.escape(file_list_path)} | xargs yard list \
19
+ --private \
20
+ --protected \
21
+ -b #{Shellwords.escape(dir)}
22
+ CMD
23
+ cmd = cmd.tr("\n", ' ')
24
+ cmd = cmd.gsub('yard list', "yard list --query #{query}")
25
+
26
+ shell(cmd)
27
+ end
28
+
29
+ # @return [String] yard query to find @example tags with syntax errors
30
+ def query
31
+ <<~QUERY
32
+ '
33
+ if object.has_tag?(:example)
34
+ example_tags = object.tags(:example)
35
+
36
+ example_tags.each_with_index do |example, index|
37
+ code = example.text
38
+ next if code.nil? || code.empty?
39
+
40
+ # Clean the code: strip output indicators (#=>) and everything after it
41
+ code_lines = code.split("\\n").map do |line|
42
+ line.sub(/\\s*#\\s*=>.*$/, "")
43
+ end
44
+
45
+ cleaned_code = code_lines.join("\\n").strip
46
+ next if cleaned_code.empty?
47
+
48
+ # Check if code looks incomplete (single expression without context)
49
+ # Skip validation for these cases
50
+ lines = cleaned_code.split("\\n").reject { |l| l.strip.empty? || l.strip.start_with?("#") }
51
+
52
+ # Skip if it is a single line that looks like an incomplete expression
53
+ if lines.size == 1
54
+ line = lines.first.strip
55
+ # Skip method calls, variable references, or simple expressions
56
+ # These are likely incomplete snippets showing usage
57
+ if line.match?(/^[a-z_][a-z0-9_]*(\\.| |$)/) || line.match?(/^[A-Z]/) && !line.match?(/^(class|module|def)\\s/)
58
+ next
59
+ end
60
+ end
61
+
62
+ # Try to parse the code
63
+ begin
64
+ RubyVM::InstructionSequence.compile(cleaned_code)
65
+ rescue SyntaxError => e
66
+ # Report syntax errors
67
+ example_name = example.name || "Example " + (index + 1).to_s
68
+ puts object.file + ":" + object.line.to_s + ": " + object.title
69
+ puts "syntax_error"
70
+ puts example_name
71
+ puts e.message
72
+ rescue => e
73
+ # Other errors (like NameError, ArgumentError) are fine
74
+ # We only check syntax, not semantics
75
+ next
76
+ end
77
+ end
78
+ end
79
+ false
80
+ '
81
+ QUERY
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,42 @@
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
+ # ExampleSyntax validator
9
+ #
10
+ # Validates Ruby syntax in `@example` tags using RubyVM::InstructionSequence.compile().
11
+ # This validator ensures that code examples in documentation are syntactically valid.
12
+ # It automatically strips output indicators (`#=>`) and skips incomplete single-line
13
+ # snippets. This validator is enabled by default.
14
+ #
15
+ # @example Bad - Invalid Ruby syntax in example
16
+ # # @example
17
+ # # def broken(
18
+ # # # missing closing
19
+ # def my_method
20
+ # end
21
+ #
22
+ # @example Good - Valid Ruby syntax in example
23
+ # # @example
24
+ # # def my_method(name)
25
+ # # puts name
26
+ # # end
27
+ # def my_method(name)
28
+ # end
29
+ #
30
+ # ## Configuration
31
+ #
32
+ # To disable this validator:
33
+ #
34
+ # Tags/ExampleSyntax:
35
+ # Enabled: false
36
+ #
37
+ module ExampleSyntax
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -77,14 +77,14 @@ module Yard
77
77
  # @return [String] tags names for which we want to check the invalid tags
78
78
  # types definitions
79
79
  def checked_tags_names
80
- validated_tags = config.validator_config('Tags/InvalidTypes', 'ValidatedTags')
80
+ validated_tags = config_or_default('ValidatedTags')
81
81
  query_array(validated_tags)
82
82
  end
83
83
 
84
84
  # @return [String] extra names that we allow for types definitions in a yard
85
85
  # query acceptable form
86
86
  def allowed_types_code
87
- extra_types = config.validator_config('Tags/InvalidTypes', 'ExtraTypes') || []
87
+ extra_types = config_or_default('ExtraTypes')
88
88
  query_array(ALLOWED_DEFAULTS + extra_types)
89
89
  end
90
90
 
@@ -4,7 +4,31 @@ module Yard
4
4
  module Lint
5
5
  module Validators
6
6
  module Tags
7
- # InvalidTypes validator module
7
+ # InvalidTypes validator
8
+ #
9
+ # Detects invalid or malformed type annotations in YARD tags. This validator
10
+ # checks that type specifications follow YARD's type syntax rules and that
11
+ # type names are valid. This validator is enabled by default.
12
+ #
13
+ # @example Bad - Invalid type syntax
14
+ # # @param name [String | Integer] the name (wrong pipe syntax)
15
+ # # @return [Array[String]] invalid nested syntax
16
+ # def process(name)
17
+ # end
18
+ #
19
+ # @example Good - Valid type syntax
20
+ # # @param name [String, Integer] the name (comma-separated union)
21
+ # # @return [Array<String>] valid nested syntax
22
+ # def process(name)
23
+ # end
24
+ #
25
+ # ## Configuration
26
+ #
27
+ # To disable this validator:
28
+ #
29
+ # Tags/InvalidTypes:
30
+ # Enabled: false
31
+ #
8
32
  module InvalidTypes
9
33
  end
10
34
  end
@@ -67,14 +67,12 @@ module Yard
67
67
 
68
68
  # @return [Array<String>] tags that should only appear on methods
69
69
  def checked_tags
70
- config.validator_config('Tags/MeaninglessTag', 'CheckedTags') || %w[param option]
70
+ config_or_default('CheckedTags')
71
71
  end
72
72
 
73
73
  # @return [Array<String>] object types that shouldn't have method-only tags
74
74
  def invalid_object_types
75
- config.validator_config('Tags/MeaninglessTag', 'InvalidObjectTypes') || %w[
76
- class module constant
77
- ]
75
+ config_or_default('InvalidObjectTypes')
78
76
  end
79
77
  end
80
78
  end
@@ -4,9 +4,37 @@ module Yard
4
4
  module Lint
5
5
  module Validators
6
6
  module Tags
7
- # MeaninglessTag validator module
8
- # Detects @param and @option tags on classes, modules, or constants
9
- # (these tags only make sense on methods)
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
+ #
10
38
  module MeaninglessTag
11
39
  end
12
40
  end
@@ -29,13 +29,14 @@ module Yard
29
29
  # @return [String] yard query to find methods with options parameter
30
30
  # but no @option tags
31
31
  def query
32
+ parameter_names = config_parameter_names
32
33
  <<~QUERY
33
34
  '
34
35
  if object.is_a?(YARD::CodeObjects::MethodObject)
35
36
  # Check if method has a parameter named "options" or "opts"
36
37
  has_options_param = object.parameters.any? do |param|
37
38
  param_name = param[0].to_s.gsub(/[*:]/, '')
38
- ['options', 'opts', 'kwargs'].include?(param_name)
39
+ #{parameter_names.inspect}.include?(param_name)
39
40
  end
40
41
 
41
42
  if has_options_param
@@ -52,6 +53,11 @@ module Yard
52
53
  '
53
54
  QUERY
54
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
55
61
  end
56
62
  end
57
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
@@ -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