reek 5.0.2 → 5.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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -70
  3. data/.rubocop_todo.yml +63 -0
  4. data/.simplecov +4 -1
  5. data/CHANGELOG.md +5 -0
  6. data/Gemfile +12 -17
  7. data/README.md +21 -29
  8. data/Rakefile +2 -2
  9. data/docs/templates/default/docstring/setup.rb +3 -0
  10. data/features/command_line_interface/options.feature +5 -3
  11. data/features/configuration_files/schema_validation.feature +3 -3
  12. data/features/configuration_files/show_configuration_file.feature +44 -0
  13. data/features/rake_task/rake_task.feature +1 -1
  14. data/features/reports/json.feature +3 -3
  15. data/features/reports/reports.feature +4 -4
  16. data/features/reports/yaml.feature +3 -3
  17. data/features/rspec_matcher.feature +1 -0
  18. data/features/step_definitions/sample_file_steps.rb +6 -8
  19. data/features/todo_list.feature +39 -26
  20. data/lib/reek.rb +7 -0
  21. data/lib/reek/ast/node.rb +4 -0
  22. data/lib/reek/ast/sexp_extensions/if.rb +20 -0
  23. data/lib/reek/ast/sexp_extensions/methods.rb +1 -0
  24. data/lib/reek/cli/application.rb +25 -0
  25. data/lib/reek/cli/command/todo_list_command.rb +17 -7
  26. data/lib/reek/cli/options.rb +21 -14
  27. data/lib/reek/code_comment.rb +2 -0
  28. data/lib/reek/configuration/app_configuration.rb +0 -3
  29. data/lib/reek/configuration/configuration_converter.rb +4 -4
  30. data/lib/reek/configuration/directory_directives.rb +1 -0
  31. data/lib/reek/configuration/schema_validator.rb +1 -0
  32. data/lib/reek/context/method_context.rb +1 -0
  33. data/lib/reek/context/module_context.rb +5 -4
  34. data/lib/reek/context/visibility_tracker.rb +7 -4
  35. data/lib/reek/context_builder.rb +1 -0
  36. data/lib/reek/detector_repository.rb +1 -0
  37. data/lib/reek/errors/incomprehensible_source_error.rb +2 -2
  38. data/lib/reek/errors/syntax_error.rb +4 -0
  39. data/lib/reek/examiner.rb +1 -0
  40. data/lib/reek/report/text_report.rb +1 -0
  41. data/lib/reek/smell_detectors/control_parameter.rb +13 -107
  42. data/lib/reek/smell_detectors/control_parameter_helpers/call_in_condition_finder.rb +91 -0
  43. data/lib/reek/smell_detectors/control_parameter_helpers/candidate.rb +38 -0
  44. data/lib/reek/smell_detectors/control_parameter_helpers/control_parameter_finder.rb +94 -0
  45. data/lib/reek/smell_detectors/duplicate_method_call.rb +1 -0
  46. data/lib/reek/smell_detectors/feature_envy.rb +2 -0
  47. data/lib/reek/smell_detectors/irresponsible_module.rb +1 -0
  48. data/lib/reek/smell_detectors/long_parameter_list.rb +1 -0
  49. data/lib/reek/smell_detectors/manual_dispatch.rb +1 -0
  50. data/lib/reek/smell_detectors/missing_safe_method.rb +1 -0
  51. data/lib/reek/smell_detectors/nested_iterators.rb +1 -0
  52. data/lib/reek/smell_detectors/repeated_conditional.rb +1 -0
  53. data/lib/reek/smell_detectors/too_many_instance_variables.rb +1 -0
  54. data/lib/reek/smell_detectors/too_many_methods.rb +1 -0
  55. data/lib/reek/smell_detectors/too_many_statements.rb +1 -0
  56. data/lib/reek/smell_detectors/uncommunicative_variable_name.rb +2 -0
  57. data/lib/reek/smell_detectors/unused_parameters.rb +1 -0
  58. data/lib/reek/source/source_locator.rb +1 -0
  59. data/lib/reek/spec/should_reek_of.rb +2 -2
  60. data/lib/reek/spec/should_reek_only_of.rb +1 -0
  61. data/lib/reek/spec/smell_matcher.rb +1 -0
  62. data/lib/reek/tree_dresser.rb +1 -0
  63. data/lib/reek/version.rb +1 -1
  64. data/samples/smelly_source/ruby.rb +368 -0
  65. data/spec/factories/factories.rb +10 -9
  66. data/spec/performance/reek/smell_detectors/runtime_speed_spec.rb +17 -0
  67. data/spec/quality/documentation_spec.rb +40 -0
  68. data/spec/reek/ast/sexp_extensions_spec.rb +20 -20
  69. data/spec/reek/cli/application_spec.rb +29 -0
  70. data/spec/reek/cli/command/todo_list_command_spec.rb +64 -46
  71. data/spec/reek/configuration/app_configuration_spec.rb +8 -8
  72. data/spec/reek/configuration/configuration_file_finder_spec.rb +3 -3
  73. data/spec/reek/configuration/schema_validator_spec.rb +10 -10
  74. data/spec/reek/detector_repository_spec.rb +2 -2
  75. data/spec/reek/smell_detectors/control_parameter_spec.rb +17 -0
  76. data/spec/reek/source/source_locator_spec.rb +0 -2
  77. data/spec/spec_helper.rb +2 -0
  78. data/tasks/configuration.rake +2 -1
  79. data/tasks/test.rake +4 -0
  80. metadata +11 -5
  81. data/ataru_setup.rb +0 -13
  82. data/tasks/ataru.rake +0 -5
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './call_in_condition_finder'
4
+ require_relative '../../ast/node'
5
+
6
+ module Reek
7
+ module SmellDetectors
8
+ module ControlParameterHelpers
9
+ # Finds cases of ControlParameter in a particular node for a particular parameter
10
+ class ControlParameterFinder
11
+ CONDITIONAL_NODE_TYPES = [:if, :case, :and, :or].freeze
12
+ #
13
+ # @param node [Reek::AST::Node] the node in our current scope,
14
+ # e.g. s(:def, :alfa,
15
+ # s(:args,
16
+ # s(:arg, :bravo),
17
+ # @param parameter [Symbol] the parameter name in question
18
+ # e.g. in the example above this would be :bravo
19
+ #
20
+ def initialize(node, parameter)
21
+ @node = node
22
+ @parameter = parameter
23
+ end
24
+
25
+ #
26
+ # @return [Array<Reek::AST::Node>] all nodes where the parameter is used for control flow
27
+ #
28
+ def find_matches
29
+ return [] if legitimate_uses?
30
+
31
+ nested_finders.flat_map(&:find_matches) + uses_of_param_in_condition
32
+ end
33
+
34
+ #
35
+ # @return [Boolean] true if the parameter is not used for control flow
36
+ #
37
+ def legitimate_uses?
38
+ return true if CallInConditionFinder.new(node, parameter).uses_param_in_call_in_condition?
39
+ return true if parameter_used_in_body?
40
+ return true if nested_finders.any?(&:legitimate_uses?)
41
+
42
+ false
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :node, :parameter
48
+
49
+ #
50
+ # @return [Boolean] if the parameter is used in the body of the method
51
+ # e.g. this
52
+ #
53
+ # def alfa(bravo)
54
+ # puts bravo
55
+ # if bravo then charlie end
56
+ # end
57
+ #
58
+ # would cause this method to return true because of the "puts bravo"
59
+ #
60
+ def parameter_used_in_body?
61
+ nodes = node.body_nodes([:lvar], CONDITIONAL_NODE_TYPES)
62
+ nodes.any? { |lvar_node| lvar_node.var_name == parameter }
63
+ end
64
+
65
+ #
66
+ # @return [Array<ControlParameterFinder>]
67
+ #
68
+ def nested_finders
69
+ @nested_finders ||= conditional_nodes.flat_map do |node|
70
+ self.class.new(node, parameter)
71
+ end
72
+ end
73
+
74
+ #
75
+ # @return [Array<Reek::AST::Node>] all nodes where the parameter is part of a condition,
76
+ # e.g. [s(:lvar, :charlie)]
77
+ #
78
+ def uses_of_param_in_condition
79
+ condition = node.condition
80
+ return [] unless condition
81
+
82
+ condition.each_node(:lvar).select { |inner| inner.var_name == parameter }
83
+ end
84
+
85
+ #
86
+ # @return [Array<Reek::AST::Node>] the conditional nodes scoped below the current node
87
+ #
88
+ def conditional_nodes
89
+ node.body_nodes(CONDITIONAL_NODE_TYPES)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -123,6 +123,7 @@ module Reek
123
123
  context.local_nodes(:send, [:mlhs]) do |call_node|
124
124
  next if call_node.object_creation_call?
125
125
  next if simple_method_call? call_node
126
+
126
127
  result[call_node].record(call_node)
127
128
  end
128
129
  context.local_nodes(:block) do |call_node|
@@ -45,6 +45,7 @@ module Reek
45
45
  def sniff
46
46
  return [] if context.singleton_method? || context.module_function?
47
47
  return [] unless context.references_self?
48
+
48
49
  envious_receivers.map do |name, lines|
49
50
  smell_warning(
50
51
  lines: lines,
@@ -61,6 +62,7 @@ module Reek
61
62
 
62
63
  def envious_receivers
63
64
  return {} if refs.self_is_max?
65
+
64
66
  refs.most_popular
65
67
  end
66
68
  end
@@ -21,6 +21,7 @@ module Reek
21
21
  #
22
22
  def sniff
23
23
  return [] if descriptive_context? || context.namespace_module?
24
+
24
25
  [smell_warning(
25
26
  lines: [source_line],
26
27
  message: 'has no descriptive comment')]
@@ -35,6 +35,7 @@ module Reek
35
35
  def sniff
36
36
  count = expression.arg_names.length
37
37
  return [] if count <= max_allowed_params
38
+
38
39
  [smell_warning(
39
40
  lines: [source_line],
40
41
  message: "has #{count} parameters",
@@ -22,6 +22,7 @@ module Reek
22
22
  def sniff
23
23
  smelly_nodes = context.local_nodes(:send).select { |node| node.name == :respond_to? }
24
24
  return [] if smelly_nodes.empty?
25
+
25
26
  lines = smelly_nodes.map(&:line)
26
27
  [smell_warning(lines: lines, message: MESSAGE)]
27
28
  end
@@ -64,6 +64,7 @@ module Reek
64
64
  return false unless method_sexp.ends_with_bang?
65
65
  return false if ignore_method? method_sexp
66
66
  return false if version_without_bang_exists? method_sexp
67
+
67
68
  true
68
69
  end
69
70
 
@@ -96,6 +96,7 @@ module Reek
96
96
  # @quality :reek:TooManyStatements { max_statements: 6 }
97
97
  def scout(exp:, depth:)
98
98
  return [] unless exp
99
+
99
100
  # Find all non-nested blocks in this expression
100
101
  exp.each_node([:block], [:block]).flat_map do |iterator|
101
102
  new_depth = increment_depth(iterator, depth)
@@ -77,6 +77,7 @@ module Reek
77
77
  collector = proc do |node|
78
78
  next unless (condition = node.condition)
79
79
  next if condition == BLOCK_GIVEN_CONDITION
80
+
80
81
  result[condition].push(condition.line)
81
82
  end
82
83
  [:if, :case].each { |stmt| context.local_nodes(stmt, &collector) }
@@ -37,6 +37,7 @@ module Reek
37
37
  variables = context.local_nodes(:ivasgn, [:or_asgn]).map(&:name)
38
38
  count = variables.uniq.size
39
39
  return [] if count <= max_allowed_ivars
40
+
40
41
  [smell_warning(
41
42
  lines: [source_line],
42
43
  message: "has at least #{count} instance variables",
@@ -39,6 +39,7 @@ module Reek
39
39
  # TODO: Only checks instance methods!
40
40
  actual = context.node_instance_methods.length
41
41
  return [] if actual <= max_allowed_methods
42
+
42
43
  [smell_warning(
43
44
  lines: [source_line],
44
45
  message: "has at least #{actual} methods",
@@ -30,6 +30,7 @@ module Reek
30
30
  def sniff
31
31
  count = context.number_of_statements
32
32
  return [] if count <= max_allowed_statements
33
+
33
34
  [smell_warning(
34
35
  lines: [source_line],
35
36
  message: "has approx #{count} statements",
@@ -111,6 +111,7 @@ module Reek
111
111
  end
112
112
 
113
113
  return unless arg_search_exp
114
+
114
115
  args_nodes = arg_search_exp.each_node(:args, [:class, :module, :defs, :def])
115
116
 
116
117
  args_nodes.each do |args_node|
@@ -133,6 +134,7 @@ module Reek
133
134
  def record_variable_name(exp, symbol, accumulator)
134
135
  varname = symbol.to_s.sub(/^\*/, '')
135
136
  return if varname == ''
137
+
136
138
  var = varname.to_sym
137
139
  accumulator[var].push(exp.line)
138
140
  end
@@ -16,6 +16,7 @@ module Reek
16
16
  #
17
17
  def sniff
18
18
  return [] if context.uses_super_with_implicit_arguments?
19
+
19
20
  context.unused_params.map do |param|
20
21
  name = param.name.to_s
21
22
  smell_warning(
@@ -56,6 +56,7 @@ module Reek
56
56
  if options.force_exclusion?
57
57
  path.ascend do |ascendant|
58
58
  break true if path_excluded?(ascendant)
59
+
59
60
  false
60
61
  end
61
62
  else
@@ -20,7 +20,7 @@ module Reek
20
20
  configuration = Configuration::AppConfiguration.default)
21
21
  @smell_type = smell_type.to_s
22
22
  @smell_details = smell_details
23
- configuration.load_values(Configuration::AppConfiguration::DETECTORS_KEY =>
23
+ configuration.load_values(DETECTORS_KEY =>
24
24
  { smell_type => { SmellConfiguration::ENABLED_KEY => true } })
25
25
  @configuration = configuration
26
26
  end
@@ -36,7 +36,7 @@ module Reek
36
36
 
37
37
  def with_config(config_hash)
38
38
  new_configuration = Configuration::AppConfiguration.default
39
- new_configuration.load_values(Configuration::AppConfiguration::DETECTORS_KEY =>
39
+ new_configuration.load_values(DETECTORS_KEY =>
40
40
  { smell_type => config_hash })
41
41
  self.class.new(smell_type, smell_details, new_configuration)
42
42
  end
@@ -20,6 +20,7 @@ module Reek
20
20
  self.examiner = examiner
21
21
  self.warnings = examiner.smells
22
22
  return false if warnings.empty?
23
+
23
24
  warnings.all? { |warning| SmellMatcher.new(warning).matches?(smell_type) }
24
25
  end
25
26
 
@@ -39,6 +39,7 @@ module Reek
39
39
  parameter_keys = other_attributes.keys - COMPARABLE_ATTRIBUTES
40
40
  extra_keys = parameter_keys - smell_warning.parameters.keys
41
41
  return if extra_keys.empty?
42
+
42
43
  raise ArgumentError, "The attribute '#{extra_keys.first}' is not available for comparison"
43
44
  end
44
45
 
@@ -41,6 +41,7 @@ module Reek
41
41
  # @quality :reek:TooManyStatements { max_statements: 6 }
42
42
  def dress(sexp, comment_map)
43
43
  return sexp unless sexp.is_a? ::Parser::AST::Node
44
+
44
45
  type = sexp.type
45
46
  children = sexp.children.map { |child| dress(child, comment_map) }
46
47
  comments = comment_map[sexp]
@@ -8,6 +8,6 @@ module Reek
8
8
  # @public
9
9
  module Version
10
10
  # @public
11
- STRING = '5.0.2'
11
+ STRING = '5.1.0'
12
12
  end
13
13
  end
@@ -0,0 +1,368 @@
1
+ module CodeRay
2
+ module Scanners
3
+
4
+ # This scanner is really complex, since Ruby _is_ a complex language!
5
+ #
6
+ # It tries to highlight 100% of all common code,
7
+ # and 90% of strange codes.
8
+ #
9
+ # It is optimized for HTML highlighting, and is not very useful for
10
+ # parsing or pretty printing.
11
+ #
12
+ # For now, I think it's better than the scanners in VIM or Syntax, or
13
+ # any highlighter I was able to find, except Caleb's RubyLexer.
14
+ #
15
+ # I hope it's also better than the rdoc/irb lexer.
16
+ class Ruby < Scanner
17
+
18
+ include Streamable
19
+
20
+ register_for :ruby
21
+ file_extension 'rb'
22
+
23
+ helper :patterns
24
+
25
+ private
26
+ def scan_tokens tokens, options
27
+ last_token_dot = false
28
+ value_expected = true
29
+ heredocs = nil
30
+ last_state = nil
31
+ state = :initial
32
+ depth = nil
33
+ inline_block_stack = []
34
+
35
+ patterns = Patterns # avoid constant lookup
36
+
37
+ until eos?
38
+ match = nil
39
+ kind = nil
40
+
41
+ if state.instance_of? patterns::StringState
42
+ # {{{
43
+ match = scan_until(state.pattern) || scan_until(/\z/)
44
+ tokens << [match, :content] unless match.empty?
45
+ break if eos?
46
+
47
+ if state.heredoc and self[1] # end of heredoc
48
+ match = getch.to_s
49
+ match << scan_until(/$/) unless eos?
50
+ tokens << [match, :delimiter]
51
+ tokens << [:close, state.type]
52
+ state = state.next_state
53
+ next
54
+ end
55
+
56
+ case match = getch
57
+
58
+ when state.delim
59
+ if state.paren
60
+ state.paren_depth -= 1
61
+ if state.paren_depth > 0
62
+ tokens << [match, :nesting_delimiter]
63
+ next
64
+ end
65
+ end
66
+ tokens << [match, :delimiter]
67
+ if state.type == :regexp and not eos?
68
+ modifiers = scan(/#{patterns::REGEXP_MODIFIERS}/ox)
69
+ tokens << [modifiers, :modifier] unless modifiers.empty?
70
+ end
71
+ tokens << [:close, state.type]
72
+ value_expected = false
73
+ state = state.next_state
74
+
75
+ when '\\'
76
+ if state.interpreted
77
+ if esc = scan(/ #{patterns::ESCAPE} /ox)
78
+ tokens << [match + esc, :char]
79
+ else
80
+ tokens << [match, :error]
81
+ end
82
+ else
83
+ case m = getch
84
+ when state.delim, '\\'
85
+ tokens << [match + m, :char]
86
+ when nil
87
+ tokens << [match, :error]
88
+ else
89
+ tokens << [match + m, :content]
90
+ end
91
+ end
92
+
93
+ when '#'
94
+ case peek(1)
95
+ when '{'
96
+ inline_block_stack << [state, depth, heredocs]
97
+ value_expected = true
98
+ state = :initial
99
+ depth = 1
100
+ tokens << [:open, :inline]
101
+ tokens << [match + getch, :inline_delimiter]
102
+ when '$', '@'
103
+ tokens << [match, :escape]
104
+ last_state = state # scan one token as normal code, then return here
105
+ state = :initial
106
+ else
107
+ raise_inspect 'else-case # reached; #%p not handled' % peek(1), tokens
108
+ end
109
+
110
+ when state.paren
111
+ state.paren_depth += 1
112
+ tokens << [match, :nesting_delimiter]
113
+
114
+ when /#{patterns::REGEXP_SYMBOLS}/ox
115
+ tokens << [match, :function]
116
+
117
+ else
118
+ raise_inspect 'else-case " reached; %p not handled, state = %p' % [match, state], tokens
119
+
120
+ end
121
+ next
122
+ # }}}
123
+ else
124
+ # {{{
125
+ if match = scan(/[ \t\f]+/)
126
+ kind = :space
127
+ match << scan(/\s*/) unless eos? or heredocs
128
+ tokens << [match, kind]
129
+ next
130
+
131
+ elsif match = scan(/\\?\n/)
132
+ kind = :space
133
+ if match == "\n"
134
+ value_expected = true # FIXME not quite true
135
+ state = :initial if state == :undef_comma_expected
136
+ end
137
+ if heredocs
138
+ unscan # heredoc scanning needs \n at start
139
+ state = heredocs.shift
140
+ tokens << [:open, state.type]
141
+ heredocs = nil if heredocs.empty?
142
+ next
143
+ else
144
+ match << scan(/\s*/) unless eos?
145
+ end
146
+ tokens << [match, kind]
147
+ next
148
+
149
+ elsif match = scan(/\#.*/) or
150
+ ( bol? and match = scan(/#{patterns::RUBYDOC_OR_DATA}/o) )
151
+ kind = :comment
152
+ value_expected = true
153
+ tokens << [match, kind]
154
+ next
155
+
156
+ elsif state == :initial
157
+
158
+ # IDENTS #
159
+ if match = scan(/#{patterns::METHOD_NAME}/o)
160
+ if last_token_dot
161
+ kind = if match[/^[A-Z]/] and not match?(/\(/) then :constant else :ident end
162
+ else
163
+ kind = patterns::IDENT_KIND[match]
164
+ if kind == :ident and match[/^[A-Z]/] and not match[/[!?]$/] and not match?(/\(/)
165
+ kind = :constant
166
+ elsif kind == :reserved
167
+ state = patterns::DEF_NEW_STATE[match]
168
+ end
169
+ end
170
+ ## experimental!
171
+ value_expected = :set if
172
+ patterns::REGEXP_ALLOWED[match] or check(/#{patterns::VALUE_FOLLOWS}/o)
173
+
174
+ elsif last_token_dot and match = scan(/#{patterns::METHOD_NAME_OPERATOR}/o)
175
+ kind = :ident
176
+ value_expected = :set if check(/#{patterns::VALUE_FOLLOWS}/o)
177
+
178
+ # OPERATORS #
179
+ elsif not last_token_dot and match = scan(/ \.\.\.? | (?:\.|::)() | [,\(\)\[\]\{\}] | ==?=? /x)
180
+ if match !~ / [.\)\]\}] /x or match =~ /\.\.\.?/
181
+ value_expected = :set
182
+ end
183
+ last_token_dot = :set if self[1]
184
+ kind = :operator
185
+ unless inline_block_stack.empty?
186
+ case match
187
+ when '{'
188
+ depth += 1
189
+ when '}'
190
+ depth -= 1
191
+ if depth == 0 # closing brace of inline block reached
192
+ state, depth, heredocs = inline_block_stack.pop
193
+ tokens << [match, :inline_delimiter]
194
+ kind = :inline
195
+ match = :close
196
+ end
197
+ end
198
+ end
199
+
200
+ elsif match = scan(/ ['"] /mx)
201
+ tokens << [:open, :string]
202
+ kind = :delimiter
203
+ state = patterns::StringState.new :string, match == '"', match # important for streaming
204
+
205
+ elsif match = scan(/#{patterns::INSTANCE_VARIABLE}/o)
206
+ kind = :instance_variable
207
+
208
+ elsif value_expected and match = scan(/\//)
209
+ tokens << [:open, :regexp]
210
+ kind = :delimiter
211
+ interpreted = true
212
+ state = patterns::StringState.new :regexp, interpreted, match
213
+
214
+ elsif match = scan(/#{patterns::NUMERIC}/o)
215
+ kind = if self[1] then :float else :integer end
216
+
217
+ elsif match = scan(/#{patterns::SYMBOL}/o)
218
+ case delim = match[1]
219
+ when ?', ?"
220
+ tokens << [:open, :symbol]
221
+ tokens << [':', :symbol]
222
+ match = delim.chr
223
+ kind = :delimiter
224
+ state = patterns::StringState.new :symbol, delim == ?", match
225
+ else
226
+ kind = :symbol
227
+ end
228
+
229
+ elsif match = scan(/ [-+!~^]=? | [*|&]{1,2}=? | >>? /x)
230
+ value_expected = :set
231
+ kind = :operator
232
+
233
+ elsif value_expected and match = scan(/#{patterns::HEREDOC_OPEN}/o)
234
+ indented = self[1] == '-'
235
+ quote = self[3]
236
+ delim = self[quote ? 4 : 2]
237
+ kind = patterns::QUOTE_TO_TYPE[quote]
238
+ tokens << [:open, kind]
239
+ tokens << [match, :delimiter]
240
+ match = :close
241
+ heredoc = patterns::StringState.new kind, quote != '\'', delim, (indented ? :indented : :linestart )
242
+ heredocs ||= [] # create heredocs if empty
243
+ heredocs << heredoc
244
+
245
+ elsif value_expected and match = scan(/#{patterns::FANCY_START_CORRECT}/o)
246
+ kind, interpreted = *patterns::FancyStringType.fetch(self[1]) do
247
+ raise_inspect 'Unknown fancy string: %%%p' % k, tokens
248
+ end
249
+ tokens << [:open, kind]
250
+ state = patterns::StringState.new kind, interpreted, self[2]
251
+ kind = :delimiter
252
+
253
+ elsif value_expected and match = scan(/#{patterns::CHARACTER}/o)
254
+ kind = :integer
255
+
256
+ elsif match = scan(/ [\/%]=? | <(?:<|=>?)? | [?:;] /x)
257
+ value_expected = :set
258
+ kind = :operator
259
+
260
+ elsif match = scan(/`/)
261
+ if last_token_dot
262
+ kind = :operator
263
+ else
264
+ tokens << [:open, :shell]
265
+ kind = :delimiter
266
+ state = patterns::StringState.new :shell, true, match
267
+ end
268
+
269
+ elsif match = scan(/#{patterns::GLOBAL_VARIABLE}/o)
270
+ kind = :global_variable
271
+
272
+ elsif match = scan(/#{patterns::CLASS_VARIABLE}/o)
273
+ kind = :class_variable
274
+
275
+ else
276
+ kind = :error
277
+ match = getch
278
+
279
+ end
280
+
281
+ elsif state == :def_expected
282
+ state = :initial
283
+ if match = scan(/(?>#{patterns::METHOD_NAME_EX})(?!\.|::)/o)
284
+ kind = :method
285
+ else
286
+ next
287
+ end
288
+
289
+ elsif state == :undef_expected
290
+ state = :undef_comma_expected
291
+ if match = scan(/#{patterns::METHOD_NAME_EX}/o)
292
+ kind = :method
293
+ elsif match = scan(/#{patterns::SYMBOL}/o)
294
+ case delim = match[1]
295
+ when ?', ?"
296
+ tokens << [:open, :symbol]
297
+ tokens << [':', :symbol]
298
+ match = delim.chr
299
+ kind = :delimiter
300
+ state = patterns::StringState.new :symbol, delim == ?", match
301
+ state.next_state = :undef_comma_expected
302
+ else
303
+ kind = :symbol
304
+ end
305
+ else
306
+ state = :initial
307
+ next
308
+ end
309
+
310
+ elsif state == :undef_comma_expected
311
+ if match = scan(/,/)
312
+ kind = :operator
313
+ state = :undef_expected
314
+ else
315
+ state = :initial
316
+ next
317
+ end
318
+
319
+ elsif state == :module_expected
320
+ if match = scan(/<</)
321
+ kind = :operator
322
+ else
323
+ state = :initial
324
+ if match = scan(/ (?:#{patterns::IDENT}::)* #{patterns::IDENT} /ox)
325
+ kind = :class
326
+ else
327
+ next
328
+ end
329
+ end
330
+
331
+ end
332
+ # }}}
333
+
334
+ value_expected = value_expected == :set
335
+ last_token_dot = last_token_dot == :set
336
+
337
+ if $DEBUG and not kind
338
+ raise_inspect 'Error token %p in line %d' %
339
+ [[match, kind], line], tokens, state
340
+ end
341
+ raise_inspect 'Empty token', tokens unless match
342
+
343
+ tokens << [match, kind]
344
+
345
+ if last_state
346
+ state = last_state
347
+ last_state = nil
348
+ end
349
+ end
350
+ end
351
+
352
+ inline_block_stack << [state] if state.is_a? patterns::StringState
353
+ until inline_block_stack.empty?
354
+ this_block = inline_block_stack.pop
355
+ tokens << [:close, :inline] if this_block.size > 1
356
+ state = this_block.first
357
+ tokens << [:close, state.type]
358
+ end
359
+
360
+ tokens
361
+ end
362
+
363
+ end
364
+
365
+ end
366
+ end
367
+
368
+ # vim:fdm=marker