rubocop 0.93.1 → 1.0.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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/config/default.yml +59 -54
- data/lib/rubocop.rb +0 -2
- data/lib/rubocop/cli/command/version.rb +1 -1
- data/lib/rubocop/config.rb +4 -0
- data/lib/rubocop/config_loader.rb +19 -2
- data/lib/rubocop/config_loader_resolver.rb +7 -5
- data/lib/rubocop/config_validator.rb +7 -6
- data/lib/rubocop/cop/badge.rb +9 -24
- data/lib/rubocop/cop/base.rb +16 -1
- data/lib/rubocop/cop/commissioner.rb +34 -20
- data/lib/rubocop/cop/correctors/percent_literal_corrector.rb +1 -1
- data/lib/rubocop/cop/layout/class_structure.rb +7 -0
- data/lib/rubocop/cop/layout/space_around_operators.rb +4 -1
- data/lib/rubocop/cop/layout/trailing_whitespace.rb +37 -13
- data/lib/rubocop/cop/lint/to_json.rb +1 -1
- data/lib/rubocop/cop/metrics/parameter_lists.rb +4 -1
- data/lib/rubocop/cop/naming/binary_operator_parameter_name.rb +1 -1
- data/lib/rubocop/cop/security/open.rb +12 -10
- data/lib/rubocop/cop/style/accessor_grouping.rb +1 -1
- data/lib/rubocop/cop/style/format_string_token.rb +47 -2
- data/lib/rubocop/cop/style/method_call_with_args_parentheses.rb +10 -13
- data/lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb +6 -11
- data/lib/rubocop/cop/style/method_call_with_args_parentheses/require_parentheses.rb +7 -11
- data/lib/rubocop/cop/style/redundant_parentheses.rb +4 -0
- data/lib/rubocop/cop/style/redundant_self.rb +3 -0
- data/lib/rubocop/cop/style/safe_navigation.rb +16 -4
- data/lib/rubocop/cop/style/string_concatenation.rb +13 -1
- data/lib/rubocop/cop/style/trailing_underscore_variable.rb +3 -1
- data/lib/rubocop/formatter/offense_count_formatter.rb +1 -1
- data/lib/rubocop/formatter/worst_offenders_formatter.rb +1 -1
- data/lib/rubocop/options.rb +4 -1
- data/lib/rubocop/version.rb +56 -6
- metadata +4 -4
@@ -12,6 +12,9 @@ module RuboCop
|
|
12
12
|
MSG = 'Avoid parameter lists longer than %<max>d parameters. ' \
|
13
13
|
'[%<count>d/%<max>d]'
|
14
14
|
|
15
|
+
NAMED_KEYWORD_TYPES = %i[kwoptarg kwarg].freeze
|
16
|
+
private_constant :NAMED_KEYWORD_TYPES
|
17
|
+
|
15
18
|
def on_args(node)
|
16
19
|
count = args_count(node)
|
17
20
|
return unless count > max_params
|
@@ -33,7 +36,7 @@ module RuboCop
|
|
33
36
|
if count_keyword_args?
|
34
37
|
node.children.size
|
35
38
|
else
|
36
|
-
node.children.count { |a|
|
39
|
+
node.children.count { |a| !NAMED_KEYWORD_TYPES.include?(a.type) }
|
37
40
|
end
|
38
41
|
end
|
39
42
|
|
@@ -18,7 +18,7 @@ module RuboCop
|
|
18
18
|
'name its argument `other`.'
|
19
19
|
|
20
20
|
OP_LIKE_METHODS = %i[eql? equal?].freeze
|
21
|
-
EXCLUDED = %i[+@ -@ [] []= << === `].freeze
|
21
|
+
EXCLUDED = %i[+@ -@ [] []= << === ` =~].freeze
|
22
22
|
|
23
23
|
def_node_matcher :op_method_candidate?, <<~PATTERN
|
24
24
|
(def [#op_method? $_] (args $(arg [!:other !:_other])) _)
|
@@ -3,35 +3,37 @@
|
|
3
3
|
module RuboCop
|
4
4
|
module Cop
|
5
5
|
module Security
|
6
|
-
# This cop checks for the use of `Kernel#open`.
|
6
|
+
# This cop checks for the use of `Kernel#open` and `URI.open`.
|
7
7
|
#
|
8
|
-
# `Kernel#open`
|
9
|
-
# by prefixing a pipe symbol (e.g., `open("| ls")`).
|
10
|
-
# a serious security risk by using variable input to
|
11
|
-
# `Kernel#open`. It would be better to use
|
12
|
-
# `URI#open` explicitly.
|
8
|
+
# `Kernel#open` and `URI.open` enable not only file access but also process
|
9
|
+
# invocation by prefixing a pipe symbol (e.g., `open("| ls")`).
|
10
|
+
# So, it may lead to a serious security risk by using variable input to
|
11
|
+
# the argument of `Kernel#open` and `URI.open`. It would be better to use
|
12
|
+
# `File.open`, `IO.popen` or `URI.parse#open` explicitly.
|
13
13
|
#
|
14
14
|
# @example
|
15
15
|
# # bad
|
16
16
|
# open(something)
|
17
|
+
# URI.open(something)
|
17
18
|
#
|
18
19
|
# # good
|
19
20
|
# File.open(something)
|
20
21
|
# IO.popen(something)
|
21
22
|
# URI.parse(something).open
|
22
23
|
class Open < Base
|
23
|
-
MSG = 'The use of `
|
24
|
+
MSG = 'The use of `%<receiver>sopen` is a serious security risk.'
|
24
25
|
RESTRICT_ON_SEND = %i[open].freeze
|
25
26
|
|
26
27
|
def_node_matcher :open?, <<~PATTERN
|
27
|
-
(send nil? :open $!str ...)
|
28
|
+
(send ${nil? (const {nil? cbase} :URI)} :open $!str ...)
|
28
29
|
PATTERN
|
29
30
|
|
30
31
|
def on_send(node)
|
31
|
-
open?(node) do |code|
|
32
|
+
open?(node) do |receiver, code|
|
32
33
|
return if safe?(code)
|
33
34
|
|
34
|
-
|
35
|
+
message = format(MSG, receiver: receiver ? "#{receiver.source}." : 'Kernel#')
|
36
|
+
add_offense(node.loc.selector, message: message)
|
35
37
|
end
|
36
38
|
end
|
37
39
|
|
@@ -7,7 +7,7 @@ module RuboCop
|
|
7
7
|
# By default it enforces accessors to be placed in grouped declarations,
|
8
8
|
# but it can be configured to enforce separating them in multiple declarations.
|
9
9
|
#
|
10
|
-
#
|
10
|
+
# NOTE: `Sorbet` is not compatible with "grouped" style. Consider "separated" style
|
11
11
|
# or disabling this cop.
|
12
12
|
#
|
13
13
|
# @example EnforcedStyle: grouped (default)
|
@@ -37,6 +37,27 @@ module RuboCop
|
|
37
37
|
#
|
38
38
|
# # good
|
39
39
|
# format('%s', 'Hello')
|
40
|
+
#
|
41
|
+
# It is allowed to contain unannotated token
|
42
|
+
# if the number of them is less than or equals to
|
43
|
+
# `MaxUnannotatedPlaceholdersAllowed`.
|
44
|
+
#
|
45
|
+
# @example MaxUnannotatedPlaceholdersAllowed: 0
|
46
|
+
#
|
47
|
+
# # bad
|
48
|
+
# format('%06d', 10)
|
49
|
+
# format('%s %s.', 'Hello', 'world')
|
50
|
+
#
|
51
|
+
# # good
|
52
|
+
# format('%<number>06d', number: 10)
|
53
|
+
#
|
54
|
+
# @example MaxUnannotatedPlaceholdersAllowed: 1 (default)
|
55
|
+
#
|
56
|
+
# # bad
|
57
|
+
# format('%s %s.', 'Hello', 'world')
|
58
|
+
#
|
59
|
+
# # good
|
60
|
+
# format('%06d', 10)
|
40
61
|
class FormatStringToken < Base
|
41
62
|
include ConfigurableEnforcedStyle
|
42
63
|
|
@@ -44,8 +65,12 @@ module RuboCop
|
|
44
65
|
return unless node.value.include?('%')
|
45
66
|
return if node.each_ancestor(:xstr, :regexp).any?
|
46
67
|
|
47
|
-
|
48
|
-
|
68
|
+
detections = collect_detections(node)
|
69
|
+
return if detections.empty?
|
70
|
+
return if allowed_unannotated?(detections)
|
71
|
+
|
72
|
+
detections.each do |detected_style, token_range|
|
73
|
+
if detected_style == style
|
49
74
|
correct_style_detected
|
50
75
|
else
|
51
76
|
style_detected(detected_style)
|
@@ -112,6 +137,26 @@ module RuboCop
|
|
112
137
|
yield(detected_style, token)
|
113
138
|
end
|
114
139
|
end
|
140
|
+
|
141
|
+
def collect_detections(node)
|
142
|
+
detections = []
|
143
|
+
tokens(node) do |detected_style, token_range|
|
144
|
+
unless unannotated_format?(node, detected_style)
|
145
|
+
detections << [detected_style, token_range]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
detections
|
149
|
+
end
|
150
|
+
|
151
|
+
def allowed_unannotated?(detections)
|
152
|
+
return false if detections.size > max_unannotated_placeholders_allowed
|
153
|
+
|
154
|
+
detections.all? { |detected_style,| detected_style == :unannotated }
|
155
|
+
end
|
156
|
+
|
157
|
+
def max_unannotated_placeholders_allowed
|
158
|
+
cop_config['MaxUnannotatedPlaceholdersAllowed']
|
159
|
+
end
|
115
160
|
end
|
116
161
|
end
|
117
162
|
end
|
@@ -144,25 +144,22 @@ module RuboCop
|
|
144
144
|
# # good
|
145
145
|
# Array 1
|
146
146
|
class MethodCallWithArgsParentheses < Base
|
147
|
+
require_relative 'method_call_with_args_parentheses/omit_parentheses'
|
148
|
+
require_relative 'method_call_with_args_parentheses/require_parentheses'
|
149
|
+
|
147
150
|
include ConfigurableEnforcedStyle
|
148
151
|
include IgnoredMethods
|
149
152
|
include IgnoredPattern
|
153
|
+
include RequireParentheses
|
154
|
+
include OmitParentheses
|
150
155
|
extend AutoCorrector
|
151
156
|
|
152
|
-
def
|
153
|
-
|
154
|
-
return unless style_configured?
|
155
|
-
|
156
|
-
case style
|
157
|
-
when :require_parentheses
|
158
|
-
extend RequireParentheses
|
159
|
-
when :omit_parentheses
|
160
|
-
extend OmitParentheses
|
161
|
-
end
|
157
|
+
def on_send(node)
|
158
|
+
send(style, node) # call require_parentheses or omit_parentheses
|
162
159
|
end
|
163
|
-
|
164
|
-
|
165
|
-
|
160
|
+
alias on_csend on_send
|
161
|
+
alias on_super on_send
|
162
|
+
alias on_yield on_send
|
166
163
|
|
167
164
|
private
|
168
165
|
|
@@ -7,15 +7,19 @@ module RuboCop
|
|
7
7
|
# Style omit_parentheses
|
8
8
|
module OmitParentheses
|
9
9
|
TRAILING_WHITESPACE_REGEX = /\s+\Z/.freeze
|
10
|
+
OMIT_MSG = 'Omit parentheses for method calls with arguments.'
|
11
|
+
private_constant :OMIT_MSG
|
10
12
|
|
11
|
-
|
13
|
+
private
|
14
|
+
|
15
|
+
def omit_parentheses(node)
|
12
16
|
return unless node.parenthesized?
|
13
17
|
return if node.implicit_call?
|
14
18
|
return if super_call_without_arguments?(node)
|
15
19
|
return if allowed_camel_case_method_call?(node)
|
16
20
|
return if legitimate_call_with_parentheses?(node)
|
17
21
|
|
18
|
-
add_offense(offense_range(node)) do |corrector|
|
22
|
+
add_offense(offense_range(node), message: OMIT_MSG) do |corrector|
|
19
23
|
if parentheses_at_the_end_of_multiline_call?(node)
|
20
24
|
corrector.replace(args_begin(node), ' \\')
|
21
25
|
else
|
@@ -24,20 +28,11 @@ module RuboCop
|
|
24
28
|
corrector.remove(node.loc.end)
|
25
29
|
end
|
26
30
|
end
|
27
|
-
alias on_csend on_send
|
28
|
-
alias on_super on_send
|
29
|
-
alias on_yield on_send
|
30
|
-
|
31
|
-
private
|
32
31
|
|
33
32
|
def offense_range(node)
|
34
33
|
node.loc.begin.join(node.loc.end)
|
35
34
|
end
|
36
35
|
|
37
|
-
def message(_range = nil)
|
38
|
-
'Omit parentheses for method calls with arguments.'
|
39
|
-
end
|
40
|
-
|
41
36
|
def super_call_without_arguments?(node)
|
42
37
|
node.super_type? && node.arguments.none?
|
43
38
|
end
|
@@ -6,27 +6,23 @@ module RuboCop
|
|
6
6
|
class MethodCallWithArgsParentheses
|
7
7
|
# Style require_parentheses
|
8
8
|
module RequireParentheses
|
9
|
-
|
9
|
+
REQUIRE_MSG = 'Use parentheses for method calls with arguments.'
|
10
|
+
private_constant :REQUIRE_MSG
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def require_parentheses(node)
|
10
15
|
return if ignored_method?(node.method_name)
|
11
16
|
return if matches_ignored_pattern?(node.method_name)
|
12
17
|
return if eligible_for_parentheses_omission?(node)
|
13
18
|
return unless node.arguments? && !node.parenthesized?
|
14
19
|
|
15
|
-
add_offense(node) do |corrector|
|
20
|
+
add_offense(node, message: REQUIRE_MSG) do |corrector|
|
16
21
|
corrector.replace(args_begin(node), '(')
|
17
22
|
|
18
23
|
corrector.insert_after(args_end(node), ')') unless args_parenthesized?(node)
|
19
24
|
end
|
20
25
|
end
|
21
|
-
alias on_csend on_send
|
22
|
-
alias on_super on_send
|
23
|
-
alias on_yield on_send
|
24
|
-
|
25
|
-
def message(_node = nil)
|
26
|
-
'Use parentheses for method calls with arguments.'
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
26
|
|
31
27
|
def eligible_for_parentheses_omission?(node)
|
32
28
|
node.operator_method? || node.setter_method? || ignored_macro?(node)
|
@@ -104,9 +104,13 @@ module RuboCop
|
|
104
104
|
return offense(begin_node, 'a variable') if node.variable?
|
105
105
|
return offense(begin_node, 'a constant') if node.const_type?
|
106
106
|
|
107
|
+
return offense(begin_node, 'an interpolated expression') if interpolation?(begin_node)
|
108
|
+
|
107
109
|
check_send(begin_node, node) if node.call_type?
|
108
110
|
end
|
109
111
|
|
112
|
+
def_node_matcher :interpolation?, '[^begin ^^dstr]'
|
113
|
+
|
110
114
|
def check_send(begin_node, node)
|
111
115
|
return check_unary(begin_node, node) if node.unary_operation?
|
112
116
|
|
@@ -129,6 +129,9 @@ module RuboCop
|
|
129
129
|
def allowed_send_node?(node)
|
130
130
|
@allowed_send_nodes.include?(node) ||
|
131
131
|
@local_variables_scopes[node].include?(node.method_name) ||
|
132
|
+
node.each_ancestor.any? do |ancestor|
|
133
|
+
@local_variables_scopes[ancestor].include?(node.method_name)
|
134
|
+
end ||
|
132
135
|
KERNEL_METHODS.include?(node.method_name)
|
133
136
|
end
|
134
137
|
|
@@ -142,10 +142,22 @@ module RuboCop
|
|
142
142
|
end
|
143
143
|
|
144
144
|
def comments(node)
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
145
|
+
relevant_comment_ranges(node).each.with_object([]) do |range, comments|
|
146
|
+
comments.concat(processed_source.each_comment_in_lines(range).to_a)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def relevant_comment_ranges(node)
|
151
|
+
# Get source lines ranges inside the if node that aren't inside an inner node
|
152
|
+
# Comments inside an inner node should remain attached to that node, and not
|
153
|
+
# moved.
|
154
|
+
begin_pos = node.loc.first_line
|
155
|
+
end_pos = node.loc.last_line
|
156
|
+
|
157
|
+
node.child_nodes.each.with_object([]) do |child, ranges|
|
158
|
+
ranges << (begin_pos...child.loc.first_line)
|
159
|
+
begin_pos = child.loc.last_line
|
160
|
+
end << (begin_pos...end_pos)
|
149
161
|
end
|
150
162
|
|
151
163
|
def allowed_if_condition?(node)
|
@@ -33,6 +33,10 @@ module RuboCop
|
|
33
33
|
}
|
34
34
|
PATTERN
|
35
35
|
|
36
|
+
def on_new_investigation
|
37
|
+
@corrected_nodes = nil
|
38
|
+
end
|
39
|
+
|
36
40
|
def on_send(node)
|
37
41
|
return unless string_concatenation?(node)
|
38
42
|
|
@@ -42,8 +46,12 @@ module RuboCop
|
|
42
46
|
collect_parts(topmost_plus_node, parts)
|
43
47
|
|
44
48
|
add_offense(topmost_plus_node) do |corrector|
|
45
|
-
|
49
|
+
correctable_parts = parts.none? { |part| uncorrectable?(part) }
|
50
|
+
if correctable_parts && !corrected_ancestor?(topmost_plus_node)
|
46
51
|
corrector.replace(topmost_plus_node, replacement(parts))
|
52
|
+
|
53
|
+
@corrected_nodes ||= Set.new.compare_by_identity
|
54
|
+
@corrected_nodes.add(topmost_plus_node)
|
47
55
|
end
|
48
56
|
end
|
49
57
|
end
|
@@ -80,6 +88,10 @@ module RuboCop
|
|
80
88
|
part.each_descendant(:block).any?
|
81
89
|
end
|
82
90
|
|
91
|
+
def corrected_ancestor?(node)
|
92
|
+
node.each_ancestor(:send).any? { |ancestor| @corrected_nodes&.include?(ancestor) }
|
93
|
+
end
|
94
|
+
|
83
95
|
def replacement(parts)
|
84
96
|
interpolated_parts =
|
85
97
|
parts.map do |part|
|
@@ -36,6 +36,8 @@ module RuboCop
|
|
36
36
|
MSG = 'Do not use trailing `_`s in parallel assignment. ' \
|
37
37
|
'Prefer `%<code>s`.'
|
38
38
|
UNDERSCORE = '_'
|
39
|
+
DISALLOW = %i[lvasgn splat].freeze
|
40
|
+
private_constant :DISALLOW
|
39
41
|
|
40
42
|
def on_masgn(node)
|
41
43
|
ranges = unneeded_ranges(node)
|
@@ -64,7 +66,7 @@ module RuboCop
|
|
64
66
|
|
65
67
|
def find_first_possible_offense(variables)
|
66
68
|
variables.reduce(nil) do |offense, variable|
|
67
|
-
break offense unless
|
69
|
+
break offense unless DISALLOW.include?(variable.type)
|
68
70
|
|
69
71
|
var, = *variable
|
70
72
|
var, = *var
|
data/lib/rubocop/options.rb
CHANGED
@@ -242,6 +242,9 @@ module RuboCop
|
|
242
242
|
# @api private
|
243
243
|
class OptionsValidator
|
244
244
|
class << self
|
245
|
+
SYNTAX_DEPARTMENTS = %w[Syntax Lint/Syntax].freeze
|
246
|
+
private_constant :SYNTAX_DEPARTMENTS
|
247
|
+
|
245
248
|
# Cop name validation must be done later than option parsing, so it's not
|
246
249
|
# called from within Options.
|
247
250
|
def validate_cop_list(names)
|
@@ -253,7 +256,7 @@ module RuboCop
|
|
253
256
|
names.each do |name|
|
254
257
|
next if cop_names.include?(name)
|
255
258
|
next if departments.include?(name)
|
256
|
-
next if
|
259
|
+
next if SYNTAX_DEPARTMENTS.include?(name)
|
257
260
|
|
258
261
|
raise IncorrectCopNameError, format_message_from(name, cop_names)
|
259
262
|
end
|
data/lib/rubocop/version.rb
CHANGED
@@ -3,24 +3,74 @@
|
|
3
3
|
module RuboCop
|
4
4
|
# This module holds the RuboCop version information.
|
5
5
|
module Version
|
6
|
-
STRING = '0.
|
6
|
+
STRING = '1.0.0'
|
7
7
|
|
8
8
|
MSG = '%<version>s (using Parser %<parser_version>s, '\
|
9
9
|
'rubocop-ast %<rubocop_ast_version>s, ' \
|
10
10
|
'running on %<ruby_engine>s %<ruby_version>s %<ruby_platform>s)'
|
11
11
|
|
12
|
+
CANONICAL_FEATURE_NAMES = { 'Rspec' => 'RSpec' }.freeze
|
13
|
+
|
12
14
|
# @api private
|
13
|
-
def self.version(debug: false)
|
15
|
+
def self.version(debug: false, env: nil)
|
14
16
|
if debug
|
15
|
-
format(MSG, version: STRING, parser_version: Parser::VERSION,
|
16
|
-
|
17
|
-
|
18
|
-
|
17
|
+
verbose_version = format(MSG, version: STRING, parser_version: Parser::VERSION,
|
18
|
+
rubocop_ast_version: RuboCop::AST::Version::STRING,
|
19
|
+
ruby_engine: RUBY_ENGINE, ruby_version: RUBY_VERSION,
|
20
|
+
ruby_platform: RUBY_PLATFORM)
|
21
|
+
return verbose_version unless env
|
22
|
+
|
23
|
+
extension_versions = extension_versions(env)
|
24
|
+
return verbose_version if extension_versions.empty?
|
25
|
+
|
26
|
+
<<~VERSIONS
|
27
|
+
#{verbose_version}
|
28
|
+
#{extension_versions.join("\n")}
|
29
|
+
VERSIONS
|
19
30
|
else
|
20
31
|
STRING
|
21
32
|
end
|
22
33
|
end
|
23
34
|
|
35
|
+
# @api private
|
36
|
+
def self.extension_versions(env)
|
37
|
+
env.config_store.for_pwd.loaded_features.sort.map do |loaded_feature|
|
38
|
+
next unless (match = loaded_feature.match(/rubocop-(?<feature>.*)/))
|
39
|
+
|
40
|
+
feature = match[:feature]
|
41
|
+
begin
|
42
|
+
require "rubocop/#{feature}/version"
|
43
|
+
rescue LoadError
|
44
|
+
# Not worth mentioning libs that are not installed
|
45
|
+
else
|
46
|
+
next unless (feature_version = feature_version(feature))
|
47
|
+
|
48
|
+
" - #{loaded_feature} #{feature_version}"
|
49
|
+
end
|
50
|
+
end.compact
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns feature version in one of two ways:
|
54
|
+
#
|
55
|
+
# * Find by RuboCop core version style (e.g. rubocop-performance, rubocop-rspec)
|
56
|
+
# * Find by `bundle gem` version style (e.g. rubocop-rake)
|
57
|
+
#
|
58
|
+
# @api private
|
59
|
+
def self.feature_version(feature)
|
60
|
+
capitalized_feature = feature.capitalize
|
61
|
+
extension_name = CANONICAL_FEATURE_NAMES.fetch(capitalized_feature, capitalized_feature)
|
62
|
+
|
63
|
+
# Find by RuboCop core version style (e.g. rubocop-performance, rubocop-rspec)
|
64
|
+
RuboCop.const_get(extension_name)::Version::STRING
|
65
|
+
rescue NameError
|
66
|
+
begin
|
67
|
+
# Find by `bundle gem` version style (e.g. rubocop-rake, rubocop-packaging)
|
68
|
+
RuboCop.const_get(extension_name)::VERSION
|
69
|
+
rescue NameError
|
70
|
+
# noop
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
24
74
|
# @api private
|
25
75
|
def self.document_version
|
26
76
|
STRING.match('\d+\.\d+').to_s
|