yard-lint 1.6.1 → 1.7.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -1
  3. data/README.md +4 -3
  4. data/bin/yard-lint +35 -4
  5. data/lib/yard/lint/config.rb +21 -4
  6. data/lib/yard/lint/config_loader.rb +31 -6
  7. data/lib/yard/lint/config_updater.rb +22 -1
  8. data/lib/yard/lint/config_validator.rb +2 -1
  9. data/lib/yard/lint/executor/in_process_registry.rb +58 -23
  10. data/lib/yard/lint/executor/warning_dispatcher.rb +1 -0
  11. data/lib/yard/lint/git.rb +44 -3
  12. data/lib/yard/lint/path_grouper.rb +4 -1
  13. data/lib/yard/lint/results/aggregate.rb +15 -7
  14. data/lib/yard/lint/stats_calculator.rb +8 -2
  15. data/lib/yard/lint/templates/default_config.yml +34 -1
  16. data/lib/yard/lint/templates/strict_config.yml +34 -1
  17. data/lib/yard/lint/todo_generator.rb +35 -14
  18. data/lib/yard/lint/validators/base.rb +39 -1
  19. data/lib/yard/lint/validators/documentation/blank_line_before_definition/validator.rb +17 -2
  20. data/lib/yard/lint/validators/documentation/empty_comment_line/validator.rb +5 -2
  21. data/lib/yard/lint/validators/documentation/line_length/validator.rb +1 -0
  22. data/lib/yard/lint/validators/documentation/markdown_syntax/validator.rb +66 -9
  23. data/lib/yard/lint/validators/documentation/missing_return/validator.rb +3 -3
  24. data/lib/yard/lint/validators/documentation/orphaned_doc_comment/validator.rb +79 -11
  25. data/lib/yard/lint/validators/documentation/orphaned_doc_comment.rb +4 -4
  26. data/lib/yard/lint/validators/documentation/text_substitution/validator.rb +10 -2
  27. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/config.rb +10 -1
  28. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/parser.rb +18 -4
  29. data/lib/yard/lint/validators/documentation/undocumented_method_arguments/validator.rb +38 -4
  30. data/lib/yard/lint/validators/documentation/undocumented_objects/parser.rb +6 -0
  31. data/lib/yard/lint/validators/documentation/undocumented_options/validator.rb +30 -0
  32. data/lib/yard/lint/validators/semantic/abstract_methods/result.rb +1 -0
  33. data/lib/yard/lint/validators/semantic/abstract_methods/validator.rb +43 -4
  34. data/lib/yard/lint/validators/tags/api_tags/validator.rb +11 -1
  35. data/lib/yard/lint/validators/tags/collection_type/messages_builder.rb +36 -5
  36. data/lib/yard/lint/validators/tags/collection_type/validator.rb +10 -6
  37. data/lib/yard/lint/validators/tags/example_style/validator.rb +1 -1
  38. data/lib/yard/lint/validators/tags/example_syntax/config.rb +6 -1
  39. data/lib/yard/lint/validators/tags/example_syntax/validator.rb +74 -3
  40. data/lib/yard/lint/validators/tags/forbidden_tags/validator.rb +7 -3
  41. data/lib/yard/lint/validators/tags/informal_notation/config.rb +6 -1
  42. data/lib/yard/lint/validators/tags/informal_notation/validator.rb +24 -3
  43. data/lib/yard/lint/validators/tags/invalid_types/config.rb +7 -1
  44. data/lib/yard/lint/validators/tags/invalid_types/parser.rb +22 -5
  45. data/lib/yard/lint/validators/tags/invalid_types/validator.rb +24 -10
  46. data/lib/yard/lint/validators/tags/missing_yield/validator.rb +44 -4
  47. data/lib/yard/lint/validators/tags/missing_yield.rb +8 -8
  48. data/lib/yard/lint/validators/tags/non_ascii_type/validator.rb +13 -1
  49. data/lib/yard/lint/validators/tags/option_tags/result.rb +1 -0
  50. data/lib/yard/lint/validators/tags/option_tags/validator.rb +30 -2
  51. data/lib/yard/lint/validators/tags/order/parser.rb +12 -5
  52. data/lib/yard/lint/validators/tags/order/validator.rb +7 -2
  53. data/lib/yard/lint/validators/tags/redundant_param_description/validator.rb +21 -8
  54. data/lib/yard/lint/validators/tags/tag_group_separator/parser.rb +12 -5
  55. data/lib/yard/lint/validators/tags/tag_group_separator/validator.rb +9 -7
  56. data/lib/yard/lint/validators/tags/tag_type_position/validator.rb +8 -2
  57. data/lib/yard/lint/validators/tags/type_syntax/validator.rb +1 -1
  58. data/lib/yard/lint/validators/warnings/invalid_directive_format/parser.rb +2 -2
  59. data/lib/yard/lint/validators/warnings/invalid_tag_format/parser.rb +2 -2
  60. data/lib/yard/lint/validators/warnings/syntax_error/config.rb +22 -0
  61. data/lib/yard/lint/validators/warnings/syntax_error/parser.rb +28 -0
  62. data/lib/yard/lint/validators/warnings/syntax_error/result.rb +27 -0
  63. data/lib/yard/lint/validators/warnings/syntax_error/validator.rb +15 -0
  64. data/lib/yard/lint/validators/warnings/syntax_error.rb +34 -0
  65. data/lib/yard/lint/validators/warnings/unknown_directive/parser.rb +2 -2
  66. data/lib/yard/lint/validators/warnings/unknown_parameter_name/messages_builder.rb +51 -46
  67. data/lib/yard/lint/validators/warnings/unknown_tag/messages_builder.rb +20 -3
  68. data/lib/yard/lint/validators/warnings/unknown_tag/parser.rb +2 -2
  69. data/lib/yard/lint/version.rb +1 -1
  70. data/lib/yard/lint.rb +4 -1
  71. metadata +6 -1
@@ -29,12 +29,18 @@ module Yard
29
29
 
30
30
  object.docstring.tags.each do |tag|
31
31
  next unless tags_to_check.include?(tag.tag_name)
32
- next unless tag.name && tag.text && !tag.text.strip.empty?
33
32
 
34
- param_name = tag.name
35
- description = tag.text.strip.gsub(/\.$/, '')
33
+ # Option tags carry their description on the nested pair tag;
34
+ # the documented name is the option key (e.g. ":mode"), not the
35
+ # hash parameter name
36
+ data = tag_data(tag)
37
+ param_name = data.equal?(tag) ? tag.name : data.name.to_s.sub(/\A:/, '')
38
+ next if param_name.nil? || param_name.empty?
39
+ next unless data.text && !data.text.strip.empty?
40
+
41
+ description = data.text.strip.gsub(/\.$/, '')
36
42
  word_count = description.split.length
37
- type_name = tag.types&.first&.gsub(/[<>{}\[\],]/, '')&.strip
43
+ type_name = data.types&.first&.gsub(/[<>{}\[\],]/, '')&.strip
38
44
 
39
45
  next if word_count > max_words
40
46
 
@@ -46,7 +52,7 @@ module Yard
46
52
  next unless pattern_type
47
53
 
48
54
  collector.puts "#{object.file}:#{object.line}: #{object.title}"
49
- collector.puts "#{tag.tag_name}|#{param_name}|#{tag.text.strip}|#{type_name || ''}|#{pattern_type}|#{word_count}"
55
+ collector.puts "#{tag.tag_name}|#{param_name}|#{data.text.strip}|#{type_name || ''}|#{pattern_type}|#{word_count}"
50
56
  end
51
57
  end
52
58
  # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
@@ -68,7 +74,9 @@ module Yard
68
74
  # rubocop:disable Metrics/AbcSize, Metrics/ParameterLists
69
75
  def detect_pattern(param_name, description, type_name, word_count, articles, generic_terms, connectors, low_value_verbs, patterns)
70
76
  desc_parts = description.split
71
- articles_re = /^(#{articles.join('|')})/i
77
+ # Anchored on both ends: only a whole-word article counts, not
78
+ # any word that merely starts with one (authenticated, theme)
79
+ articles_re = /\A(#{articles.map { |a| Regexp.escape(a) }.join('|')})\z/i
72
80
 
73
81
  # ArticleParam pattern
74
82
  if patterns['ArticleParam'] && word_count <= 3 && desc_parts.length == 2
@@ -97,9 +105,14 @@ module Yard
97
105
  end
98
106
  end
99
107
 
100
- # ParamToVerb pattern
108
+ # ParamToVerb pattern: "<param> to <low-value verb>" (e.g.
109
+ # "user to update"). The third word must actually be a low-value
110
+ # verb, otherwise meaningful noun phrases like "path to file" are
111
+ # wrongly flagged.
101
112
  if patterns['ParamToVerb'] && word_count <= 4 && desc_parts.length == 3
102
- if desc_parts[0].downcase == param_name.downcase && desc_parts[1].downcase == 'to'
113
+ if desc_parts[0].downcase == param_name.downcase &&
114
+ desc_parts[1].downcase == 'to' &&
115
+ low_value_verbs.include?(desc_parts[2].downcase)
103
116
  return 'param_to_verb'
104
117
  end
105
118
  end
@@ -39,12 +39,19 @@ module Yard
39
39
  end
40
40
 
41
41
  base_hash.delete_if { |_key, value| value == 'valid' }
42
- separator_data = base_hash.values.map(&:last)
43
42
 
44
- Validators::Documentation::UndocumentedMethodArguments::Parser
45
- .new
46
- .call(base_hash.values.map(&:first).join("\n"))
47
- .each.with_index { |element, index| element[:separators] = separator_data[index] }
43
+ location_parser = Validators::Documentation::UndocumentedMethodArguments::Parser.new
44
+
45
+ # Parse each location together with its own separators so that an
46
+ # unparseable location line drops only its own offense instead of
47
+ # shifting the separators of all offenses that follow it
48
+ base_hash.values.filter_map do |location, separators|
49
+ element = location_parser.call(location).first
50
+ next unless element
51
+
52
+ element[:separators] = separators
53
+ element
54
+ end
48
55
  end
49
56
 
50
57
  private
@@ -21,7 +21,9 @@ module Yard
21
21
  #
22
22
  # @return [void]
23
23
  def in_process_query(object, collector)
24
- return if object.is_alias?
24
+ # is_alias? exists only on method objects; on namespace objects
25
+ # YARD's method_missing raises NameError, so guard by type first
26
+ return if object.type == :method && object.is_alias?
25
27
 
26
28
  docstring = object.docstring.all
27
29
  return if docstring.nil? || docstring.empty?
@@ -52,17 +54,17 @@ module Yard
52
54
  had_blank_line = true
53
55
 
54
56
  lines.each do |line|
55
- stripped = line.strip
56
-
57
- if stripped.empty?
57
+ if line.strip.empty?
58
58
  had_blank_line = true
59
59
  next
60
60
  end
61
61
 
62
- if stripped.start_with?('@')
63
- tag_name = stripped.match(/^@(\S+)/)&.captures&.first
64
- next unless tag_name
62
+ # A real YARD tag begins at column 0 of the docstring. Indented
63
+ # @-leading lines are tag continuation or @example/code content
64
+ # (e.g. an instance variable like `@result`), not new tag groups.
65
+ tag_name = line[/\A@(\S+)/, 1]
65
66
 
67
+ if tag_name
66
68
  current_group = group_for_tag(tag_name)
67
69
 
68
70
  if previous_group && current_group != previous_group && !had_blank_line
@@ -33,8 +33,10 @@ module Yard
33
33
  (start_line...(end_line - 1)).reverse_each do |line_num|
34
34
  line = source_lines[line_num].to_s.strip
35
35
 
36
- # Skip empty lines
37
- next if line.empty?
36
+ # A blank line means the comment above is detached from the
37
+ # definition - YARD does not attach it as the docstring, so stop
38
+ # scanning rather than reaching up to an unrelated comment block.
39
+ break if line.empty?
38
40
 
39
41
  # Stop if we hit code (non-comment line)
40
42
  break unless line.start_with?('#')
@@ -43,6 +45,10 @@ module Yard
43
45
  next unless line.include?('@')
44
46
 
45
47
  checked_tags.each do |tag_name|
48
+ # The @option grammar is always "name [Type] :key", so the name
49
+ # must precede the type - it is not subject to type_first.
50
+ next if style == 'type_first' && tag_name == 'option'
51
+
46
52
  if style == 'type_first'
47
53
  # Detect: @tag_name word [Type] (violation when type_first is enforced)
48
54
  pattern = /@#{tag_name}\s+(\w+)\s+\[([^\]]+)\]/
@@ -29,7 +29,7 @@ module Yard
29
29
  # @return [void]
30
30
  def in_process_query(object, collector)
31
31
  validated_tags = config.validator_config('Tags/TypeSyntax', 'ValidatedTags') ||
32
- %w[param option return yieldreturn]
32
+ Config.defaults['ValidatedTags']
33
33
 
34
34
  all_typed_tags(object.docstring, validated_tags).each do |tag|
35
35
  next unless tag.types
@@ -10,9 +10,9 @@ module Yard
10
10
  # Set of regexps for detecting warnings reported by YARD stats
11
11
  self.regexps = {
12
12
  general: /^\[warn\]: Invalid directive format/,
13
- message: /\[warn\]: (.*) in file/,
13
+ message: %r{\[warn\]: (.*?) in file},
14
14
  location: /in file `(.*)`/,
15
- line: /line (\d*)/
15
+ line: %r{near line (\d+)}
16
16
  }.freeze
17
17
  end
18
18
  end
@@ -10,9 +10,9 @@ module Yard
10
10
  # Set of regexps for detecting warnings reported by YARD stats
11
11
  self.regexps = {
12
12
  general: /^\[warn\]: Invalid tag format/,
13
- message: /\[warn\]: (.*) in file/,
13
+ message: %r{\[warn\]: (.*?) in file},
14
14
  location: /in file `(.*)`/,
15
- line: /line (\d*)/
15
+ line: %r{near line (\d+)}
16
16
  }.freeze
17
17
  end
18
18
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ # Validators for checking YARD warnings
7
+ module Warnings
8
+ # Validator for detecting files YARD could not parse
9
+ module SyntaxError
10
+ # Configuration for SyntaxError validator
11
+ class Config < ::Yard::Lint::Validators::Config
12
+ self.id = :syntax_error
13
+ self.defaults = {
14
+ 'Enabled' => true,
15
+ 'Severity' => 'error'
16
+ }.freeze
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Warnings
7
+ module SyntaxError
8
+ # Parser for SyntaxError warnings.
9
+ #
10
+ # YARD reports a parse failure as a single warning line of the form:
11
+ # Syntax error in `path/to/file.rb`:(LINE,COL): <description>
12
+ # (a sibling `ParserSyntaxError:` line and a stack trace are also
13
+ # emitted, but the `general` pattern matches only the first so each
14
+ # failure yields exactly one offense).
15
+ class Parser < ::Yard::Lint::Parsers::OneLineBase
16
+ # Set of regexps for detecting syntax-error warnings reported by YARD
17
+ self.regexps = {
18
+ general: /^\[warn\]: Syntax error in /,
19
+ message: /:\(\d+,\d+\):\s*(.+)$/,
20
+ location: /Syntax error in `(.+?)`/,
21
+ line: /:\((\d+),\d+\):/
22
+ }.freeze
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Warnings
7
+ module SyntaxError
8
+ # Result object for SyntaxError validation
9
+ class Result < Results::Base
10
+ self.default_severity = 'error'
11
+ self.offense_type = 'line'
12
+ self.offense_name = 'SyntaxError'
13
+
14
+ # Build human-readable message for a SyntaxError offense
15
+ # @param offense [Hash] offense data with :message key
16
+ # @return [String] formatted message
17
+ def build_message(offense)
18
+ base = 'File could not be parsed by YARD and was skipped'
19
+ detail = offense[:message]
20
+ detail && !detail.empty? ? "#{base}: #{detail}" : base
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ module Warnings
7
+ module SyntaxError
8
+ # Validator for detecting files YARD could not parse (syntax errors)
9
+ class Validator < Base
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yard
4
+ module Lint
5
+ module Validators
6
+ # Validators for checking YARD warnings
7
+ module Warnings
8
+ # SyntaxError validator
9
+ #
10
+ # Flags Ruby files that YARD could not parse because of a syntax error.
11
+ # Such files are silently skipped by YARD - they contribute no objects and
12
+ # therefore no documentation offenses - so without this validator a run
13
+ # could pass (exit 0) over code that does not even parse. This validator
14
+ # surfaces the parser error as an offense (severity `error` by default),
15
+ # making the run exit non-zero. Enabled by default.
16
+ #
17
+ # For example, a file whose method signature is missing its closing
18
+ # parenthesis (`def foo(x` followed by a body and `end`) fails to parse;
19
+ # YARD reports `Syntax error in <file>:(LINE,COL): ...` and this validator
20
+ # turns that into an offense. A file that parses cleanly produces nothing.
21
+ #
22
+ # ## Configuration
23
+ #
24
+ # To disable this validator:
25
+ #
26
+ # Warnings/SyntaxError:
27
+ # Enabled: false
28
+ #
29
+ module SyntaxError
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -10,9 +10,9 @@ module Yard
10
10
  # Set of regexps for detecting warnings reported by YARD stats
11
11
  self.regexps = {
12
12
  general: /^\[warn\]: Unknown directive.*@!.*near line/,
13
- message: /\[warn\]: (.*) in file/,
13
+ message: %r{\[warn\]: (.*?) in file},
14
14
  location: /in file `(.*)`/,
15
- line: /line (\d*)/
15
+ line: %r{near line (\d+)}
16
16
  }.freeze
17
17
  end
18
18
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'did_you_mean'
4
- require 'shellwords'
5
4
 
6
5
  module Yard
7
6
  module Lint
@@ -52,14 +51,8 @@ module Yard
52
51
 
53
52
  line_num = line.to_i
54
53
 
55
- # First, try to parse directly from the Ruby source file
56
- # This is faster and doesn't require YARD to be fully loaded
57
- params = parse_parameters_from_source(file, line_num)
58
- return params unless params.empty?
59
-
60
- # Fallback: Query YARD list for the method
61
- # This requires YARD to parse the file first
62
- fetch_parameters_via_yard(file, line_num)
54
+ # Parse directly from the Ruby source file.
55
+ parse_parameters_from_source(file, line_num)
63
56
  rescue StandardError => e
64
57
  # If anything goes wrong, just return empty array (no suggestion)
65
58
  warn "Failed to fetch parameters: #{e.message}" if ENV['DEBUG']
@@ -73,8 +66,10 @@ module Yard
73
66
  def parse_parameters_from_source(file, line)
74
67
  return [] unless File.exist?(file)
75
68
 
76
- # Calculate the search range (line numbers are 1-indexed)
77
- start_line = [(line - 15), 1].max
69
+ # The warning is reported at the method's def line, so start
70
+ # there - scanning earlier would pick up an unrelated method
71
+ # defined in the preceding lines.
72
+ start_line = [line, 1].max
78
73
  end_line = line + 5
79
74
 
80
75
  # Only read the lines in the relevant range to avoid loading the whole file
@@ -92,12 +87,15 @@ module Yard
92
87
  param_lines = []
93
88
 
94
89
  lines.each do |source_line|
90
+ # The method name may include a receiver (def self.foo, def
91
+ # obj.foo) or be an operator, so match anything up to the
92
+ # opening parenthesis rather than a bare \w+.
95
93
  # Match single-line method definitions: def method_name(param1, param2)
96
- if source_line =~ /^\s*def\s+\w+\s*\((.*?)\)/
94
+ if source_line =~ /^\s*def\s+[^(]+\((.*?)\)/
97
95
  params_str = ::Regexp.last_match(1)
98
96
  return extract_parameter_names(params_str)
99
97
  # Match start of multi-line method definition: def method_name(
100
- elsif source_line =~ /^\s*def\s+\w+\s*\((.*)$/
98
+ elsif source_line =~ /^\s*def\s+[^(]+\((.*)$/
101
99
  in_multiline_def = true
102
100
  param_lines << ::Regexp.last_match(1)
103
101
  next
@@ -111,7 +109,7 @@ module Yard
111
109
  params_str = params_str[/\A(.*?)\)/, 1] || params_str
112
110
  return extract_parameter_names(params_str)
113
111
  end
114
- elsif source_line.match?(/^\s*def\s+\w+\s*$/)
112
+ elsif source_line.match?(/^\s*def\s+[^(]+$/)
115
113
  # Method with no parameters
116
114
  return []
117
115
  end
@@ -130,38 +128,42 @@ module Yard
130
128
  def extract_parameter_names(params_str)
131
129
  return [] if params_str.nil? || params_str.strip.empty?
132
130
 
133
- params_str.split(',').map do |param|
134
- # Remove default values: "name = 'default'" => "name"
135
- param = param.split('=').first
136
- # Remove type annotations: "name:" => "name"
137
- param = param.delete(':')
138
- # Remove splat and block symbols: "*args", "**kwargs", "&block"
139
- param = param.delete('*&')
140
- # Strip whitespace
141
- param.strip
142
- end.reject(&:empty?)
131
+ split_top_level_params(params_str).filter_map do |param|
132
+ # Strip leading splat/block markers (*args, **kwargs, &block).
133
+ name = param.strip.sub(/\A[*&]+/, '')
134
+ # The name is everything up to the first ':' (keyword) or '='
135
+ # (default), so a symbol default like `mode: :fast` or a default
136
+ # containing commas like `list = [1, 2]` is not mangled.
137
+ name = name[/\A[^:=]+/].to_s.strip
138
+ name.empty? ? nil : name
139
+ end
143
140
  end
144
141
 
145
- # Fetch parameters via YARD list command (fallback method)
146
- # @param file [String] file path
147
- # @param line [Integer] line number
148
- # @return [Array<String>] array of parameter names
149
- def fetch_parameters_via_yard(file, line)
150
- # Query YARD for the method at this location
151
- # Use Shellwords.escape to prevent command injection
152
- escaped_file = Shellwords.escape(file)
153
- query = "'type == :method && file == \"#{escaped_file}\" && line >= #{line - 15} && line <= #{line + 5}'"
154
- cmd = "yard list --query #{query} 2>/dev/null"
155
-
156
- output = `#{cmd}`.strip
157
- return [] if output.empty?
158
-
159
- # YARD list doesn't show parameters, we'd need to parse the source
160
- # So this fallback is just for validation - use source parsing instead
161
- []
162
- rescue StandardError => e
163
- warn "Failed to query YARD: #{e.message}" if ENV['DEBUG']
164
- []
142
+ # Splits a parameter string on top-level commas, respecting
143
+ # brackets so defaults like `[1, 2]` or `{a: 1}` stay intact.
144
+ # @param params_str [String] the raw parameter list
145
+ # @return [Array<String>] individual parameter substrings
146
+ def split_top_level_params(params_str)
147
+ parts = []
148
+ current = +''
149
+ depth = 0
150
+ params_str.each_char do |char|
151
+ case char
152
+ when '(', '[', '{' then depth += 1; current << char
153
+ when ')', ']', '}' then depth -= 1; current << char
154
+ when ','
155
+ if depth.zero?
156
+ parts << current
157
+ current = +''
158
+ else
159
+ current << char
160
+ end
161
+ else
162
+ current << char
163
+ end
164
+ end
165
+ parts << current
166
+ parts
165
167
  end
166
168
 
167
169
  # Find the best suggestion using DidYouMean spell checker
@@ -203,9 +205,12 @@ module Yard
203
205
  return nil unless best_match
204
206
 
205
207
  param, distance = best_match
206
- max_distance = [unknown_param.length, param.length].max / 2
208
+ # Require the edit distance to be strictly less than half the
209
+ # longer length, so short, very different names are not
210
+ # "corrected" to an unrelated parameter.
211
+ max_length = [unknown_param.length, param.length].max
207
212
 
208
- distance <= max_distance ? param : nil
213
+ distance < max_length / 2.0 ? param : nil
209
214
  end
210
215
 
211
216
  # Calculate Levenshtein distance between two strings
@@ -58,8 +58,13 @@ module Yard
58
58
  suggestion = find_suggestion(unknown_tag)
59
59
 
60
60
  if suggestion
61
+ # A directive (e.g. parse, method, attribute) must be written
62
+ # with the @! prefix; only real tags use a plain @. Rendering
63
+ # a directive suggestion as @parse would itself be an unknown
64
+ # tag.
65
+ prefix = directive_only?(suggestion) ? '@!' : '@'
61
66
  # Replace just the descriptive part before "in file"
62
- message.sub(/Unknown tag @\w+/, "Unknown tag @#{unknown_tag} (did you mean '@#{suggestion}'?)")
67
+ message.sub(/Unknown tag @\w+/, "Unknown tag @#{unknown_tag} (did you mean '#{prefix}#{suggestion}'?)")
63
68
  else
64
69
  message
65
70
  end
@@ -67,6 +72,14 @@ module Yard
67
72
 
68
73
  private
69
74
 
75
+ # Whether the given name is a YARD directive and not also a tag,
76
+ # so it must be referenced with the @! prefix.
77
+ # @param name [String] candidate tag/directive name
78
+ # @return [Boolean]
79
+ def directive_only?(name)
80
+ known_directives.include?(name) && !known_tags.include?(name)
81
+ end
82
+
70
83
  # Find the best suggestion using DidYouMean spell checker
71
84
  # @param unknown_tag [String] the unknown tag name (without @ prefix)
72
85
  # @return [String, nil] suggested tag name or nil
@@ -104,9 +117,13 @@ module Yard
104
117
  return nil unless best_match
105
118
 
106
119
  tag, distance = best_match
107
- max_distance = [unknown_tag.length, tag.length].max / 2
120
+ # Require the edit distance to be strictly less than half the
121
+ # longer length, so short, very different names like @foo/@spec
122
+ # are not "corrected" to @todo/@see (which differ by half their
123
+ # characters).
124
+ max_length = [unknown_tag.length, tag.length].max
108
125
 
109
- distance <= max_distance ? tag : nil
126
+ distance < max_length / 2.0 ? tag : nil
110
127
  end
111
128
 
112
129
  # Calculate Levenshtein distance between two strings
@@ -12,9 +12,9 @@ module Yard
12
12
  # Set of regexps for detecting warnings reported by YARD stats
13
13
  self.regexps = {
14
14
  general: /^\[warn\]: Unknown tag.*@.*near line/,
15
- message: /\[warn\]: (.*) in file/,
15
+ message: %r{\[warn\]: (.*?) in file},
16
16
  location: /in file `(.*)`/,
17
- line: /line (\d*)/
17
+ line: %r{near line (\d+)}
18
18
  }.freeze
19
19
  end
20
20
  end
@@ -3,6 +3,6 @@
3
3
  module Yard
4
4
  module Lint
5
5
  # @return [String] version of the YARD Lint gem
6
- VERSION = '1.6.1'
6
+ VERSION = '1.7.0'
7
7
  end
8
8
  end
data/lib/yard/lint.rb CHANGED
@@ -78,7 +78,10 @@ module Yard
78
78
  # Get changed files from git based on mode
79
79
  git_files = case diff[:mode]
80
80
  when :ref
81
- Git.changed_files(diff[:base_ref], path)
81
+ # When no explicit REF was given, honor the configured
82
+ # DefaultBaseRef before falling back to main/master.
83
+ base_ref = diff[:base_ref] || config&.diff_mode_default_base_ref
84
+ Git.changed_files(base_ref, path)
82
85
  when :staged
83
86
  Git.staged_files(path)
84
87
  when :changed
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yard-lint
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.1
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maciej Mensfeld
@@ -262,6 +262,11 @@ files:
262
262
  - lib/yard/lint/validators/warnings/invalid_tag_format/parser.rb
263
263
  - lib/yard/lint/validators/warnings/invalid_tag_format/result.rb
264
264
  - lib/yard/lint/validators/warnings/invalid_tag_format/validator.rb
265
+ - lib/yard/lint/validators/warnings/syntax_error.rb
266
+ - lib/yard/lint/validators/warnings/syntax_error/config.rb
267
+ - lib/yard/lint/validators/warnings/syntax_error/parser.rb
268
+ - lib/yard/lint/validators/warnings/syntax_error/result.rb
269
+ - lib/yard/lint/validators/warnings/syntax_error/validator.rb
265
270
  - lib/yard/lint/validators/warnings/unknown_directive.rb
266
271
  - lib/yard/lint/validators/warnings/unknown_directive/config.rb
267
272
  - lib/yard/lint/validators/warnings/unknown_directive/parser.rb