Class: Reek::ContextBuilder
- Inherits:
-
Object
- Object
- Reek::ContextBuilder
- 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
-
#context_tree ⇒ Object
readonly
Returns the value of attribute context_tree.
-
#current_context ⇒ Object
private
Returns the value of attribute current_context.
-
#exp ⇒ Object
readonly
private
Returns the value of attribute exp.
Instance Method Summary collapse
-
#append_new_context(klass, *args) ⇒ Context::*Context
private
Appends a new child context to the current context but does not change the current context.
-
#build(exp, parent_exp = nil) ⇒ Reek::Context::RootContext
private
Processes the given AST, memoizes it and returns a tree of nested contexts.
- #context_processor_exists?(name) ⇒ Boolean private
- #decrease_statement_count ⇒ Object private
- #handle_refinement_block(exp) ⇒ Object private
- #handle_send_for_methods(exp) ⇒ Object private
- #handle_send_for_modules(exp) ⇒ Object private
- #increase_statement_count_by(sexp) ⇒ Object private
-
#initialize(syntax_tree) ⇒ ContextBuilder
constructor
A new instance of ContextBuilder.
-
#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.
-
#process(exp) ⇒ Object
private
Handles every node for which we have no context_processor.
-
#process_begin(exp, _parent) ⇒ Object
(also: #process_kwbegin)
private
Handles ‘begin` and `kwbegin` nodes.
-
#process_block(exp, _parent) ⇒ Object
private
Handles ‘block` nodes.
-
#process_case(exp, _parent) ⇒ Object
private
Handles ‘case` nodes.
-
#process_casgn(exp, parent) ⇒ Object
private
Handles ‘casgn` (“class assign”) nodes.
-
#process_def(exp, parent) ⇒ Object
private
Handles ‘def` nodes.
-
#process_defs(exp, parent) ⇒ Object
private
Handles ‘defs` nodes (“define singleton”).
-
#process_for(exp, _parent) ⇒ Object
private
Handles ‘for` nodes.
-
#process_if(exp, _parent) ⇒ Object
private
Handles ‘if` nodes.
-
#process_ivar(exp, _parent) ⇒ Object
(also: #process_ivasgn)
private
Handles ‘ivasgn` and `ivar` nodes a.k.a.
-
#process_module(exp, _parent) ⇒ Object
(also: #process_class)
private
Handles ‘module` and `class` nodes.
-
#process_op_asgn(exp, _parent) ⇒ Object
private
Handles ‘op_asgn` nodes a.k.a.
-
#process_resbody(exp, _parent) ⇒ Object
private
Handles ‘resbody` nodes.
-
#process_rescue(exp, _parent) ⇒ Object
private
Handles ‘rescue` nodes.
-
#process_sclass(exp, _parent) ⇒ Object
private
Handles ‘sclass` nodes.
-
#process_self(_exp, _parent) ⇒ Object
private
Handles ‘self` nodes.
-
#process_send(exp, _parent) ⇒ Object
private
Handles ‘send` nodes a.k.a.
-
#process_super(exp, _parent) ⇒ Object
private
Handles ‘super` nodes a.k.a.
-
#process_when(exp, _parent) ⇒ Object
private
Handles ‘when` nodes.
-
#process_while(exp, _parent) ⇒ Object
(also: #process_until)
private
Handles ‘while` and `until` nodes.
-
#process_zsuper(_exp, _parent) ⇒ Object
private
Handles ‘zsuper` nodes a.k.a.
- #register_attributes(exp) ⇒ Object private
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_tree ⇒ Object (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_context ⇒ Object (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 |
#exp ⇒ Object (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.
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
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)
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_count ⇒ Object (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.
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; = 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; = 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(); 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 |