reek 5.0.2 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
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