Class: Reek::ContextBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/reek/context_builder.rb

Overview

Traverses an abstract syntax tree and fires events whenever it encounters specific node types.

TODO: This class is responsible for statements and reference counting. Ideally ‘ContextBuilder` would only build up the context tree and leave the statement and reference counting to the contexts.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(syntax_tree) ⇒ ContextBuilder

Returns a new instance of ContextBuilder.



30
31
32
33
34
# File 'lib/reek/context_builder.rb', line 30

def initialize(syntax_tree)
  @exp = syntax_tree
  @current_context = Context::RootContext.new(exp)
  @context_tree = build(exp)
end

Instance Attribute Details

#context_treeObject (readonly)

Returns the value of attribute context_tree.



28
29
30
# File 'lib/reek/context_builder.rb', line 28

def context_tree
  @context_tree
end

#current_contextObject (private)

Returns the value of attribute current_context.



38
39
40
# File 'lib/reek/context_builder.rb', line 38

def current_context
  @current_context
end

#expObject (readonly, private)

Returns the value of attribute exp.



39
40
41
# File 'lib/reek/context_builder.rb', line 39

def exp
  @exp
end

Instance Method Details

#append_new_context(klass, *args) ⇒ Context::*Context (private)

Appends a new child context to the current context but does not change the current context.

Parameters:

  • klass (Context::*Context)

    context class

  • args

    arguments for the class initializer

Returns:



513
514
515
516
517
# File 'lib/reek/context_builder.rb', line 513

def append_new_context(klass, *args)
  klass.new(*args).tap do |new_context|
    new_context.register_with_parent(current_context)
  end
end

#build(exp, parent_exp = nil) ⇒ Reek::Context::RootContext (private)

Processes the given AST, memoizes it and returns a tree of nested contexts.

For example this ruby code:

class Car; def drive; end; end

would get compiled into this AST:

(class
  (const nil :Car) nil
  (def :drive
    (args) nil))

Processing this AST would result in a context tree where each node contains the outer context, the AST and the child contexts. The top node is always Reek::Context::RootContext. Using the example above, the tree would look like this:

RootContext -> children: 1 ModuleContext -> children: 1 MethodContext

Returns:



63
64
65
66
67
68
69
70
71
# File 'lib/reek/context_builder.rb', line 63

def build(exp, parent_exp = nil)
  context_processor = "process_#{exp.type}"
  if context_processor_exists?(context_processor)
    send(context_processor, exp, parent_exp)
  else
    process exp
  end
  current_context
end

#context_processor_exists?(name) ⇒ Boolean (private)

Returns:

  • (Boolean)


477
478
479
# File 'lib/reek/context_builder.rb', line 477

def context_processor_exists?(name)
  self.class.private_method_defined?(name)
end

#decrease_statement_countObject (private)



486
487
488
# File 'lib/reek/context_builder.rb', line 486

def decrease_statement_count
  current_context.statement_counter.decrease_by 1
end

#handle_refinement_block(exp) ⇒ Object (private)



519
520
521
522
523
# File 'lib/reek/context_builder.rb', line 519

def handle_refinement_block(exp)
  inside_new_context(Context::RefinementContext, exp) do
    process(exp)
  end
end

#handle_send_for_methods(exp) ⇒ Object (private)



531
532
533
534
# File 'lib/reek/context_builder.rb', line 531

def handle_send_for_methods(exp)
  append_new_context(Context::SendContext, exp, exp.name)
  current_context.record_call_to(exp)
end

#handle_send_for_modules(exp) ⇒ Object (private)



525
526
527
528
529
# File 'lib/reek/context_builder.rb', line 525

def handle_send_for_modules(exp)
  arg_names = exp.args.map { |arg| arg.children.first }
  current_context.track_visibility(exp.name, arg_names)
  register_attributes(exp)
end

#increase_statement_count_by(sexp) ⇒ Object (private)



482
483
484
# File 'lib/reek/context_builder.rb', line 482

def increase_statement_count_by(sexp)
  current_context.statement_counter.increase_by sexp
end

#inside_new_context(klass, *args) { ... } ⇒ Object (private)

Stores a reference to the current context, creates a nested new one, yields to the given block and then restores the previous context.

Parameters:

  • klass (Context::*Context)

    context class

  • args

    arguments for the class initializer

Yields:

  • block



497
498
499
500
501
502
503
# File 'lib/reek/context_builder.rb', line 497

def inside_new_context(klass, *args)
  new_context = append_new_context(klass, *args)

  orig, self.current_context = current_context, new_context
  yield
  self.current_context = orig
end

#process(exp) ⇒ Object (private)

Handles every node for which we have no context_processor.



75
76
77
# File 'lib/reek/context_builder.rb', line 75

def process(exp)
  exp.children.grep(AST::Node).each { |child| build(child, exp) }
end

#process_begin(exp, _parent) ⇒ Object (private) Also known as: process_kwbegin

Handles ‘begin` and `kwbegin` nodes. `begin` nodes are created implicitly e.g. when parsing method bodies (see example below), `kwbegin` nodes are created by explicitly using the `begin` keyword.

An input example that would trigger this method would be:

def foo; call_me(); @x = 5; end

In this case the whole method body would be hanging below the ‘begin` node.

Counts all statements in the method body.

At the end we subtract one statement because the surrounding context was already counted as one (e.g. via ‘process_def`).



294
295
296
297
298
# File 'lib/reek/context_builder.rb', line 294

def process_begin(exp, _parent)
  increase_statement_count_by(exp.children)
  decrease_statement_count
  process(exp)
end

#process_block(exp, _parent) ⇒ Object (private)

Handles ‘block` nodes.

An input example that would trigger this method would be:

list.map { |element| puts element }

Counts non-empty blocks as one statement.

A refinement block is handled differently and causes a RefinementContext to be opened.



270
271
272
273
274
275
276
277
# File 'lib/reek/context_builder.rb', line 270

def process_block(exp, _parent)
  increase_statement_count_by(exp.block)
  if exp.call.name == :refine
    handle_refinement_block(exp)
  else
    process(exp)
  end
end

#process_case(exp, _parent) ⇒ Object (private)

Handles ‘case` nodes.

An input example that would trigger this method would be:

foo = 5 case foo when 1..100

puts 'In between'

else

puts 'Not sure what I got here'

end

Counts the ‘else` body.

At the end we subtract one statement because the surrounding context was already counted as one (e.g. via ‘process_def`).



446
447
448
449
450
# File 'lib/reek/context_builder.rb', line 446

def process_case(exp, _parent)
  increase_statement_count_by(exp.else_body)
  decrease_statement_count
  process(exp)
end

#process_casgn(exp, parent) ⇒ Object (private)

Handles ‘casgn` (“class assign”) nodes.

An input example that would trigger this method would be:

Foo = Class.new Bar


108
109
110
111
112
113
114
# File 'lib/reek/context_builder.rb', line 108

def process_casgn(exp, parent)
  if exp.defines_module?
    process_module(exp, parent)
  else
    process(exp)
  end
end

#process_def(exp, parent) ⇒ Object (private)

Handles ‘def` nodes.

An input example that would trigger this method would be:

def call_me; foo = 2; bar = 5; end

Given the above example we would count 2 statements overall.



124
125
126
127
128
129
# File 'lib/reek/context_builder.rb', line 124

def process_def(exp, parent)
  inside_new_context(current_context.method_context_class, exp, parent) do
    increase_statement_count_by(exp.body)
    process(exp)
  end
end

#process_defs(exp, parent) ⇒ Object (private)

Handles ‘defs` nodes (“define singleton”).

An input example that would trigger this method would be:

def self.call_me; foo = 2; bar = 5; end

Given the above example we would count 2 statements overall.



139
140
141
142
143
144
# File 'lib/reek/context_builder.rb', line 139

def process_defs(exp, parent)
  inside_new_context(Context::SingletonMethodContext, exp, parent) do
    increase_statement_count_by(exp.body)
    process(exp)
  end
end

#process_for(exp, _parent) ⇒ Object (private)

Handles ‘for` nodes.

An input example that would trigger this method would be:

for i in [1,2,3,4]

puts i

end

Counts the ‘for` body as one statement.

At the end we subtract one statement because the surrounding context was already counted as one (e.g. via ‘process_def`).

children` below refers to the `while` body (so `puts i` from above)



366
367
368
369
370
# File 'lib/reek/context_builder.rb', line 366

def process_for(exp, _parent)
  increase_statement_count_by(exp.children[2])
  decrease_statement_count
  process(exp)
end

#process_if(exp, _parent) ⇒ Object (private)

Handles ‘if` nodes.

An input example that would trigger this method would be:

if a > 5 && b < 3

puts 'bingo'

else

3

end

Counts the ‘if` body as one statement and the `else` body as another statement.

At the end we subtract one statement because the surrounding context was already counted as one (e.g. via ‘process_def`).

children` refers to the `if` body (so `puts ’bingo’‘ from above) and `children` to the `else` body (so `3` from above), which might be nil.



320
321
322
323
324
325
326
# File 'lib/reek/context_builder.rb', line 320

def process_if(exp, _parent)
  children = exp.children
  increase_statement_count_by(children[1])
  increase_statement_count_by(children[2])
  decrease_statement_count
  process(exp)
end

#process_ivar(exp, _parent) ⇒ Object (private) Also known as: process_ivasgn

Handles ‘ivasgn` and `ivar` nodes a.k.a. nodes related to instance variables.

An input example that would trigger this method would be:

@item = 5

for instance assignments (‘ivasgn`) and

call_me(@item)

for just using instance variables (‘ivar`).

We record one reference to ‘self`.



197
198
199
200
# File 'lib/reek/context_builder.rb', line 197

def process_ivar(exp, _parent)
  current_context.record_use_of_self
  process(exp)
end

#process_module(exp, _parent) ⇒ Object (private) Also known as: process_class

Handles ‘module` and `class` nodes.



81
82
83
84
85
# File 'lib/reek/context_builder.rb', line 81

def process_module(exp, _parent)
  inside_new_context(Context::ModuleContext, exp) do
    process(exp)
  end
end

#process_op_asgn(exp, _parent) ⇒ Object (private)

Handles ‘op_asgn` nodes a.k.a. Ruby’s assignment operators.

An input example that would trigger this method would be:

x += 5

or

x *= 3

We record one reference to ‘x` given the example above.



178
179
180
181
# File 'lib/reek/context_builder.rb', line 178

def process_op_asgn(exp, _parent)
  current_context.record_call_to(exp)
  process(exp)
end

#process_resbody(exp, _parent) ⇒ Object (private)

Handles ‘resbody` nodes.

An input example that would trigger this method would be:

def simple

raise ArgumentError, 'raising...'

rescue => e

puts 'rescued!'

end

Counts the exception capturing and every statement related to it.

So ‘exp.children` from the code below would be an array with the following 2 elements: [

(lvasgn :e),
(send nil :puts (str "rescued!"))

]

which thus counts as 2 statements. ‘exp` would be the whole `rescue` body. See `process_rescue` for additional reference.



424
425
426
427
# File 'lib/reek/context_builder.rb', line 424

def process_resbody(exp, _parent)
  increase_statement_count_by(exp.children[1..].compact)
  process(exp)
end

#process_rescue(exp, _parent) ⇒ Object (private)

Handles ‘rescue` nodes.

An input example that would trigger this method would be:

def simple

raise ArgumentError, 'raising...'

rescue => e

puts 'rescued!'

end

Counts everything before the ‘rescue` body as one statement.

At the end we subtract one statement because the surrounding context was already counted as one (e.g. via ‘process_def`).

‘exp.children.first` below refers to everything before the actual `rescue` which would be the

raise ArgumentError, ‘raising…’

in the example above. ‘exp` would be the whole method body wrapped under a `rescue` node. See `process_resbody` for additional reference.



396
397
398
399
400
# File 'lib/reek/context_builder.rb', line 396

def process_rescue(exp, _parent)
  increase_statement_count_by(exp.children.first)
  decrease_statement_count
  process(exp)
end

#process_sclass(exp, _parent) ⇒ Object (private)

Handles ‘sclass` nodes

An input example that would trigger this method would be:

class << self
end


96
97
98
99
100
# File 'lib/reek/context_builder.rb', line 96

def process_sclass(exp, _parent)
  inside_new_context(Context::GhostContext, exp) do
    process(exp)
  end
end

#process_self(_exp, _parent) ⇒ Object (private)

Handles ‘self` nodes.

An input example that would trigger this method would be:

def self.foo; end


210
211
212
# File 'lib/reek/context_builder.rb', line 210

def process_self(_exp, _parent)
  current_context.record_use_of_self
end

#process_send(exp, _parent) ⇒ Object (private)

Handles ‘send` nodes a.k.a. method calls.

An input example that would trigger this method would be:

call_me()

Besides checking if it’s a visibility modifier or an attribute writer we also record to what the method call is referring to which we later use for smell detectors like FeatureEnvy.



156
157
158
159
160
161
162
163
164
# File 'lib/reek/context_builder.rb', line 156

def process_send(exp, _parent)
  process(exp)
  case current_context
  when Context::ModuleContext
    handle_send_for_modules exp
  when Context::MethodContext
    handle_send_for_methods exp
  end
end

#process_super(exp, _parent) ⇒ Object (private)

Handles ‘super` nodes a.k.a. calls to `super` with arguments

An input example that would trigger this method would be:

def call_me; super(); end

or

def call_me; super(bar); end

but not

def call_me; super; end

and not

def call_me; super do end; end

We record one reference to ‘self`.



254
255
256
257
# File 'lib/reek/context_builder.rb', line 254

def process_super(exp, _parent)
  current_context.record_use_of_self
  process(exp)
end

#process_when(exp, _parent) ⇒ Object (private)

Handles ‘when` nodes.

An input example that would trigger this method would be:

foo = 5 case foo when (1..100)

puts 'In between'

else

puts 'Not sure what I got here'

end

Note that input like

if foo then :holla else :nope end

does not trigger this method.

Counts the ‘when` body.



472
473
474
475
# File 'lib/reek/context_builder.rb', line 472

def process_when(exp, _parent)
  increase_statement_count_by(exp.body)
  process(exp)
end

#process_while(exp, _parent) ⇒ Object (private) Also known as: process_until

Handles ‘while` and `until` nodes.

An input example that would trigger this method would be:

while x < 5

puts 'bingo'

end

Counts the ‘while` body as one statement.

At the end we subtract one statement because the surrounding context was already counted as one (e.g. via ‘process_def`).

children` below refers to the `while` body (so `puts ’bingo’‘ from above)



343
344
345
346
347
# File 'lib/reek/context_builder.rb', line 343

def process_while(exp, _parent)
  increase_statement_count_by(exp.children[1])
  decrease_statement_count
  process(exp)
end

#process_zsuper(_exp, _parent) ⇒ Object (private)

Handles ‘zsuper` nodes a.k.a. calls to `super` without any arguments but a block possibly.

An input example that would trigger this method would be:

def call_me; super; end

or

def call_me; super do end; end

but not

def call_me; super(); end

We record one reference to ‘self`.



230
231
232
# File 'lib/reek/context_builder.rb', line 230

def process_zsuper(_exp, _parent)
  current_context.record_use_of_self
end

#register_attributes(exp) ⇒ Object (private)



536
537
538
539
540
541
542
543
# File 'lib/reek/context_builder.rb', line 536

def register_attributes(exp)
  return unless exp.attribute_writer?

  klass = current_context.attribute_context_class
  exp.args.each do |arg|
    append_new_context(klass, arg, exp)
  end
end