deep-cover 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +2 -0
- data/.travis.yml +10 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +127 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/cov +43 -0
- data/bin/gemcov +8 -0
- data/bin/selfcov +21 -0
- data/bin/setup +8 -0
- data/bin/testall +88 -0
- data/deep_cover.gemspec +44 -0
- data/exe/deep-cover +6 -0
- data/future_read_me.md +108 -0
- data/lib/deep-cover.rb +1 -0
- data/lib/deep_cover.rb +11 -0
- data/lib/deep_cover/analyser.rb +24 -0
- data/lib/deep_cover/analyser/base.rb +51 -0
- data/lib/deep_cover/analyser/branch.rb +20 -0
- data/lib/deep_cover/analyser/covered_code_source.rb +31 -0
- data/lib/deep_cover/analyser/function.rb +12 -0
- data/lib/deep_cover/analyser/ignore_uncovered.rb +19 -0
- data/lib/deep_cover/analyser/node.rb +11 -0
- data/lib/deep_cover/analyser/per_char.rb +20 -0
- data/lib/deep_cover/analyser/per_line.rb +23 -0
- data/lib/deep_cover/analyser/statement.rb +31 -0
- data/lib/deep_cover/analyser/subset.rb +24 -0
- data/lib/deep_cover/auto_run.rb +49 -0
- data/lib/deep_cover/autoload_tracker.rb +75 -0
- data/lib/deep_cover/backports.rb +9 -0
- data/lib/deep_cover/base.rb +55 -0
- data/lib/deep_cover/builtin_takeover.rb +2 -0
- data/lib/deep_cover/cli/debugger.rb +93 -0
- data/lib/deep_cover/cli/deep_cover.rb +49 -0
- data/lib/deep_cover/cli/instrumented_clone_reporter.rb +105 -0
- data/lib/deep_cover/config.rb +52 -0
- data/lib/deep_cover/core_ext/autoload_overrides.rb +40 -0
- data/lib/deep_cover/core_ext/coverage_replacement.rb +26 -0
- data/lib/deep_cover/core_ext/load_overrides.rb +24 -0
- data/lib/deep_cover/core_ext/require_overrides.rb +36 -0
- data/lib/deep_cover/coverage.rb +198 -0
- data/lib/deep_cover/covered_code.rb +138 -0
- data/lib/deep_cover/custom_requirer.rb +93 -0
- data/lib/deep_cover/node.rb +8 -0
- data/lib/deep_cover/node/arguments.rb +50 -0
- data/lib/deep_cover/node/assignments.rb +250 -0
- data/lib/deep_cover/node/base.rb +99 -0
- data/lib/deep_cover/node/begin.rb +25 -0
- data/lib/deep_cover/node/block.rb +53 -0
- data/lib/deep_cover/node/boolean.rb +22 -0
- data/lib/deep_cover/node/branch.rb +28 -0
- data/lib/deep_cover/node/case.rb +94 -0
- data/lib/deep_cover/node/collections.rb +21 -0
- data/lib/deep_cover/node/const.rb +10 -0
- data/lib/deep_cover/node/def.rb +38 -0
- data/lib/deep_cover/node/empty_body.rb +21 -0
- data/lib/deep_cover/node/exceptions.rb +74 -0
- data/lib/deep_cover/node/if.rb +36 -0
- data/lib/deep_cover/node/keywords.rb +84 -0
- data/lib/deep_cover/node/literals.rb +77 -0
- data/lib/deep_cover/node/loops.rb +72 -0
- data/lib/deep_cover/node/mixin/can_augment_children.rb +65 -0
- data/lib/deep_cover/node/mixin/check_completion.rb +16 -0
- data/lib/deep_cover/node/mixin/child_can_be_empty.rb +25 -0
- data/lib/deep_cover/node/mixin/executed_after_children.rb +13 -0
- data/lib/deep_cover/node/mixin/execution_location.rb +56 -0
- data/lib/deep_cover/node/mixin/flow_accounting.rb +63 -0
- data/lib/deep_cover/node/mixin/has_child.rb +138 -0
- data/lib/deep_cover/node/mixin/has_child_handler.rb +73 -0
- data/lib/deep_cover/node/mixin/has_tracker.rb +44 -0
- data/lib/deep_cover/node/mixin/is_statement.rb +18 -0
- data/lib/deep_cover/node/mixin/rewriting.rb +32 -0
- data/lib/deep_cover/node/mixin/wrapper.rb +13 -0
- data/lib/deep_cover/node/module.rb +64 -0
- data/lib/deep_cover/node/root.rb +18 -0
- data/lib/deep_cover/node/send.rb +83 -0
- data/lib/deep_cover/node/splat.rb +13 -0
- data/lib/deep_cover/node/variables.rb +14 -0
- data/lib/deep_cover/parser_ext/range.rb +40 -0
- data/lib/deep_cover/reporter.rb +6 -0
- data/lib/deep_cover/reporter/istanbul.rb +151 -0
- data/lib/deep_cover/tools.rb +18 -0
- data/lib/deep_cover/tools/builtin_coverage.rb +50 -0
- data/lib/deep_cover/tools/camelize.rb +8 -0
- data/lib/deep_cover/tools/dump_covered_code.rb +32 -0
- data/lib/deep_cover/tools/execute_sample.rb +23 -0
- data/lib/deep_cover/tools/format.rb +16 -0
- data/lib/deep_cover/tools/format_char_cover.rb +18 -0
- data/lib/deep_cover/tools/format_generated_code.rb +25 -0
- data/lib/deep_cover/tools/number_lines.rb +18 -0
- data/lib/deep_cover/tools/our_coverage.rb +9 -0
- data/lib/deep_cover/tools/require_relative_dir.rb +10 -0
- data/lib/deep_cover/tools/silence_warnings.rb +15 -0
- data/lib/deep_cover/version.rb +3 -0
- metadata +326 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
require_relative 'begin'
|
2
|
+
require_relative 'variables'
|
3
|
+
module DeepCover
|
4
|
+
class Node
|
5
|
+
# Singletons
|
6
|
+
class SingletonLiteral < Node
|
7
|
+
executed_loc_keys :expression
|
8
|
+
end
|
9
|
+
True = False = Nil = SingletonLiteral
|
10
|
+
|
11
|
+
# Atoms
|
12
|
+
def self.atom(type)
|
13
|
+
::Class.new(Node) do
|
14
|
+
has_child value: type
|
15
|
+
executed_loc_keys :expression
|
16
|
+
end
|
17
|
+
end
|
18
|
+
Sym = atom(::Symbol)
|
19
|
+
Int = atom(::Integer)
|
20
|
+
Float = atom(::Float)
|
21
|
+
Complex = atom(::Complex)
|
22
|
+
Rational = atom(::Rational)
|
23
|
+
class Regopt < Node
|
24
|
+
has_extra_children options: [::Symbol]
|
25
|
+
executed_loc_keys :expression
|
26
|
+
end
|
27
|
+
|
28
|
+
class Str < Node
|
29
|
+
has_child value: ::String
|
30
|
+
|
31
|
+
def executed_loc_keys
|
32
|
+
keys = [:expression, :heredoc_body, :heredoc_end]
|
33
|
+
|
34
|
+
exp = expression
|
35
|
+
keys.delete(:expression) if exp && exp.source !~ /\S/
|
36
|
+
|
37
|
+
hd_body = loc_hash[:heredoc_body]
|
38
|
+
keys.delete(:heredoc_body) if hd_body && hd_body.source !~ /\S/
|
39
|
+
|
40
|
+
keys
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Di-atomic
|
45
|
+
class Range < Node
|
46
|
+
has_child from: Node
|
47
|
+
has_child to: Node
|
48
|
+
end
|
49
|
+
Erange = Irange = Range
|
50
|
+
|
51
|
+
# Dynamic
|
52
|
+
def self.has_evaluated_segments
|
53
|
+
has_extra_children constituents: [Str, Begin, Ivar, Cvar, Gvar, Dstr, NthRef]
|
54
|
+
end
|
55
|
+
class DynamicLiteral < Node
|
56
|
+
def executed_loc_keys
|
57
|
+
if loc_hash[:heredoc_end]
|
58
|
+
[:expression, :heredoc_end]
|
59
|
+
else
|
60
|
+
[:begin, :end]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
Dsym = Dstr = DynamicLiteral
|
65
|
+
DynamicLiteral.has_evaluated_segments
|
66
|
+
|
67
|
+
class Regexp < Node
|
68
|
+
has_evaluated_segments
|
69
|
+
has_child option: Regopt
|
70
|
+
end
|
71
|
+
|
72
|
+
class Xstr < Node
|
73
|
+
check_completion
|
74
|
+
has_evaluated_segments
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module DeepCover
|
2
|
+
class Node
|
3
|
+
class For < Node
|
4
|
+
has_tracker :body
|
5
|
+
has_child assignments: [Mlhs, VariableAssignment], flow_entry_count: -> { body.flow_entry_count if body }
|
6
|
+
has_child iterable: [Node], flow_entry_count: -> { flow_entry_count }
|
7
|
+
has_child body: Node,
|
8
|
+
can_be_empty: -> { base_node.loc.end.begin },
|
9
|
+
flow_entry_count: :body_tracker_hits,
|
10
|
+
rewrite: '((%{body_tracker};%{local}=nil;%{node}))'
|
11
|
+
check_completion
|
12
|
+
|
13
|
+
def execution_count
|
14
|
+
iterable.flow_completion_count
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Until < Node
|
19
|
+
has_tracker :body
|
20
|
+
has_child condition: Node, rewrite: '((%{node})) || (%{body_tracker};false)'
|
21
|
+
has_child body: Node,
|
22
|
+
can_be_empty: -> { base_node.loc.end.begin },
|
23
|
+
flow_entry_count: :body_tracker_hits
|
24
|
+
check_completion
|
25
|
+
|
26
|
+
def execution_count
|
27
|
+
# TODO: while this distringuishes correctly 0 vs >0, the number return is often too high
|
28
|
+
condition.flow_completion_count
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class UntilPost < Node
|
33
|
+
has_tracker :body
|
34
|
+
has_child condition: Node, rewrite: '((%{node})) || (%{body_tracker};false)'
|
35
|
+
has_child body: Kwbegin, flow_entry_count: -> { body_tracker_hits + parent.flow_entry_count }
|
36
|
+
check_completion
|
37
|
+
|
38
|
+
def execution_count
|
39
|
+
# TODO: while this distringuishes correctly 0 vs >0, the number return is often too high
|
40
|
+
body.flow_completion_count
|
41
|
+
end
|
42
|
+
# TODO: test
|
43
|
+
end
|
44
|
+
|
45
|
+
class While < Node
|
46
|
+
has_tracker :body
|
47
|
+
has_child condition: Node, rewrite: '((%{node})) && %{body_tracker}'
|
48
|
+
has_child body: Node,
|
49
|
+
can_be_empty: -> { base_node.loc.end.begin },
|
50
|
+
flow_entry_count: :body_tracker_hits
|
51
|
+
check_completion
|
52
|
+
|
53
|
+
def execution_count
|
54
|
+
# TODO: while this distringuishes correctly 0 vs >0, the number return is often too high
|
55
|
+
condition.flow_completion_count
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class WhilePost < Node
|
60
|
+
has_tracker :body
|
61
|
+
has_child condition: Node, rewrite: '((%{node})) && %{body_tracker}'
|
62
|
+
has_child body: Kwbegin, flow_entry_count: -> { body_tracker_hits + parent.flow_entry_count }
|
63
|
+
check_completion
|
64
|
+
|
65
|
+
def execution_count
|
66
|
+
# TODO: while this distringuishes correctly 0 vs >0, the number return is often too high
|
67
|
+
body.flow_completion_count
|
68
|
+
end
|
69
|
+
# TODO: test
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module DeepCover
|
2
|
+
module Node::Mixin
|
3
|
+
module CanAugmentChildren
|
4
|
+
def self.included(base)
|
5
|
+
base.has_child_handler('remap_%{name}')
|
6
|
+
base.singleton_class.prepend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
# Augment creates a covered node from the child_base_node.
|
10
|
+
# Caution: receiver is not fully constructed since it is also being augmented.
|
11
|
+
# don't call `children`
|
12
|
+
def augment_children(child_base_nodes)
|
13
|
+
missing = self.class.min_children - child_base_nodes.size
|
14
|
+
if missing > 0
|
15
|
+
child_base_nodes = [*child_base_nodes, *Array.new(missing)]
|
16
|
+
end
|
17
|
+
child_base_nodes.map.with_index do |child, child_index|
|
18
|
+
child_name = self.class.child_index_to_name(child_index, child_base_nodes.size) rescue binding.pry
|
19
|
+
|
20
|
+
if (klass = remap_child(child, child_name))
|
21
|
+
klass.new(child, parent: self, index: child_index)
|
22
|
+
else
|
23
|
+
child
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
private :augment_children
|
28
|
+
|
29
|
+
def remap_child(child, name=nil)
|
30
|
+
return unless child.is_a?(Parser::AST::Node)
|
31
|
+
class_name = Tools.camelize(child.type)
|
32
|
+
Node.const_defined?(class_name) ? Node.const_get(class_name) : Node
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
|
37
|
+
# This handles the following shortcuts:
|
38
|
+
# has_child foo: {type: NodeClass, ...}
|
39
|
+
# same as:
|
40
|
+
# has_child foo: [], remap: {type: NodeClass, ...}
|
41
|
+
# same as:
|
42
|
+
# has_child foo: [NodeClass, ...], remap: {type: NodeClass, ...}
|
43
|
+
#
|
44
|
+
def has_child(remap: nil, **h)
|
45
|
+
name, types = h.first
|
46
|
+
if types.is_a? Hash
|
47
|
+
raise "Use either remap or a hash as type but not both" if remap
|
48
|
+
remap = types
|
49
|
+
h[name] = types = []
|
50
|
+
end
|
51
|
+
if remap.is_a? Hash
|
52
|
+
type_map = remap
|
53
|
+
remap = -> (child) do
|
54
|
+
klass = type_map[child.type] if child.respond_to? :type
|
55
|
+
klass ||= type_map[child.class]
|
56
|
+
klass
|
57
|
+
end
|
58
|
+
types.concat(type_map.values).uniq!
|
59
|
+
end
|
60
|
+
super(**h, remap: remap)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'executed_after_children'
|
2
|
+
|
3
|
+
module DeepCover
|
4
|
+
module Node::Mixin
|
5
|
+
module CheckCompletion
|
6
|
+
def check_completion(outer:'(%{node})', inner:'(%{node})')
|
7
|
+
has_tracker :completion
|
8
|
+
include ExecutedAfterChildren
|
9
|
+
alias_method :flow_completion_count, :completion_tracker_hits
|
10
|
+
pre, post = outer.split('%{node}')
|
11
|
+
# The local=local is to avoid Ruby warning about "Possible use of value in void context"
|
12
|
+
define_method(:rewrite) { "#{pre}(%{local}=#{inner};%{completion_tracker};%{local}=%{local})#{post}" }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module DeepCover
|
2
|
+
module Node::Mixin
|
3
|
+
module ChildCanBeEmpty
|
4
|
+
class << self
|
5
|
+
attr_accessor :last_empty_position # Ugly hack to pass info from Handler to constructor
|
6
|
+
def included(base)
|
7
|
+
base.has_child_handler('%{name}_can_be_empty')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def remap_child(child, name=raise)
|
12
|
+
if child == nil
|
13
|
+
if (ChildCanBeEmpty.last_empty_position = child_can_be_empty(child, name))
|
14
|
+
return Node::EmptyBody
|
15
|
+
end
|
16
|
+
end
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def child_can_be_empty(_child, _name=nil)
|
21
|
+
false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module DeepCover
|
2
|
+
module Node::Mixin
|
3
|
+
# By default, nodes are considered executed if they are entered.
|
4
|
+
# Some are considered executed only if their arguments complete.
|
5
|
+
module ExecutedAfterChildren
|
6
|
+
def execution_count
|
7
|
+
last = children_nodes_in_flow_order.last
|
8
|
+
return last.flow_completion_count if last
|
9
|
+
super
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module DeepCover
|
2
|
+
module Node::Mixin
|
3
|
+
module ExecutionLocation
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
base.has_child_handler('%{name}_executed_loc_keys')
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
# Macro to define the executed_loc_keys
|
11
|
+
def executed_loc_keys(*loc_keys)
|
12
|
+
# #flatten allows passing an empty array to be explicit
|
13
|
+
loc_keys = loc_keys.flatten
|
14
|
+
define_method :executed_loc_keys do
|
15
|
+
loc_keys
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def executed_loc_keys
|
21
|
+
loc_hash.keys - [:expression]
|
22
|
+
end
|
23
|
+
|
24
|
+
def child_executed_loc_keys(_child, _child_name)
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def executed_locs
|
29
|
+
if (keys = parent.child_executed_loc_keys(self))
|
30
|
+
inherited = parent.loc_hash.values_at(*keys)
|
31
|
+
end
|
32
|
+
[ *loc_hash.values_at(*executed_loc_keys),
|
33
|
+
*inherited
|
34
|
+
].compact
|
35
|
+
end
|
36
|
+
|
37
|
+
def loc_hash
|
38
|
+
@loc_hash ||= base_node.location.to_hash
|
39
|
+
end
|
40
|
+
|
41
|
+
def expression
|
42
|
+
loc_hash[:expression]
|
43
|
+
end
|
44
|
+
|
45
|
+
def source
|
46
|
+
expression.source if expression
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns an array of character numbers (in the original buffer) that
|
50
|
+
# pertain exclusively to this node (and thus not to any children).
|
51
|
+
def proper_range
|
52
|
+
executed_locs.map(&:to_a).inject([], :+).uniq rescue binding.pry
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module DeepCover
|
2
|
+
module Node::Mixin
|
3
|
+
module FlowAccounting
|
4
|
+
def self.included(base)
|
5
|
+
base.has_child_handler('%{name}_flow_entry_count')
|
6
|
+
end
|
7
|
+
|
8
|
+
# Returns true iff it is executable and if was successfully executed
|
9
|
+
def was_executed?
|
10
|
+
# There is a rare case of non executable nodes that have important data in flow_entry_count / flow_completion_count,
|
11
|
+
# like `if cond; end`, so make sure it's actually executable first...
|
12
|
+
executable? && execution_count > 0
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the control flow entered the node.
|
16
|
+
# The control flow can then either complete normally or be interrupted
|
17
|
+
#
|
18
|
+
# Implementation: This is always the responsibility of the parent; Nodes should not override.
|
19
|
+
def flow_entry_count
|
20
|
+
parent.child_flow_entry_count(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the number of times it changed the usual control flow (e.g. raised, returned, ...)
|
24
|
+
# Implementation: This is always deduced; Nodes should not override.
|
25
|
+
def flow_interrupt_count
|
26
|
+
flow_entry_count - flow_completion_count
|
27
|
+
end
|
28
|
+
|
29
|
+
### These are refined by subclasses
|
30
|
+
|
31
|
+
# Returns true iff it is executable. Keywords like `end` are not executable, but literals like `42` are executable.
|
32
|
+
def executable?
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns number of times the node itself was "executed". Definition of executed depends on the node.
|
37
|
+
def execution_count
|
38
|
+
flow_entry_count
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the number of times the control flow succesfully left the node.
|
42
|
+
# This is the responsability of the child Node, never of the parent.
|
43
|
+
# Must be refined if the child node may have an impact on control flow (raising, branching, ...)
|
44
|
+
def flow_completion_count
|
45
|
+
last = children_nodes_in_flow_order.last
|
46
|
+
return last.flow_completion_count if last
|
47
|
+
flow_entry_count
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the number of time the control flow entered this child_node.
|
51
|
+
# This is the responsability of the Node, not of the child.
|
52
|
+
# Must be refined if the parent node may have an impact on control flow (raising, branching, ...)
|
53
|
+
def child_flow_entry_count(child, _name = nil)
|
54
|
+
prev = child.previous_sibling
|
55
|
+
if prev
|
56
|
+
prev.flow_completion_count
|
57
|
+
else
|
58
|
+
flow_entry_count
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module DeepCover
|
2
|
+
module Node::Mixin
|
3
|
+
module HasChild
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
setup_constants(base)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.setup_constants(subclass)
|
10
|
+
subclass.const_set :CHILDREN, {}
|
11
|
+
subclass.const_set :CHILDREN_TYPES, {}
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def initialize(*)
|
16
|
+
super
|
17
|
+
self.validate_children_types(children) rescue binding.pry
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate_children_types(nodes)
|
21
|
+
mismatches = self.class.check_children_types(nodes)
|
22
|
+
unless mismatches.empty?
|
23
|
+
raise TypeError, "Invalid children types for #{self.class}(type: #{self.type}): #{mismatches}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
def has_child(rest: false, **h)
|
29
|
+
raise "Needs exactly one custom named argument, got #{h.size}" if h.size != 1
|
30
|
+
name, types = h.first
|
31
|
+
raise TypeError, "Expect a Symbol for name, got a #{name.class} (#{name.inspect})" unless name.is_a?(Symbol)
|
32
|
+
update_children_const(name, rest: rest)
|
33
|
+
define_accessor(name)
|
34
|
+
add_runtime_check(name, types)
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def has_extra_children(**h)
|
39
|
+
has_child(**h, rest: true)
|
40
|
+
end
|
41
|
+
|
42
|
+
def child_index_to_name(index, nb_children)
|
43
|
+
self::CHILDREN.each do |name, i|
|
44
|
+
return name if i == index || (i == index - nb_children) ||
|
45
|
+
(i.is_a?(Range) && i.begin <= index && i.end + nb_children >= index)
|
46
|
+
end
|
47
|
+
raise IndexError, "index #{index} does not correspond to any child of #{self}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def check_children_types(nodes)
|
51
|
+
types = expected_types(nodes)
|
52
|
+
nodes_mismatches(nodes, types)
|
53
|
+
end
|
54
|
+
|
55
|
+
def min_children
|
56
|
+
self::CHILDREN.values.grep(Integer).size
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def expected_types(nodes)
|
62
|
+
self::CHILDREN.flat_map do |name, i|
|
63
|
+
type = self::CHILDREN_TYPES[name]
|
64
|
+
Array.new(nodes.values_at(i).size, type)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def nodes_mismatches(nodes, types)
|
69
|
+
nodes = nodes.dup
|
70
|
+
nodes[nodes.size...types.size] = nil
|
71
|
+
nodes.zip(types).reject do |node, type|
|
72
|
+
node_matches_type?(node, type)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def node_matches_type?(node, expected)
|
77
|
+
case expected
|
78
|
+
when :any
|
79
|
+
true
|
80
|
+
when nil
|
81
|
+
node.nil?
|
82
|
+
when Array
|
83
|
+
expected.any? {|exp| node_matches_type?(node, exp) }
|
84
|
+
when Class
|
85
|
+
node.is_a?(expected)
|
86
|
+
when Symbol
|
87
|
+
node.is_a?(Node) && node.type == expected
|
88
|
+
else
|
89
|
+
raise "Unrecognized expected type #{expected}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def inherited(subclass)
|
94
|
+
HasChild.setup_constants(subclass)
|
95
|
+
super
|
96
|
+
end
|
97
|
+
|
98
|
+
def const_missing(name)
|
99
|
+
const_set(name, self::CHILDREN.fetch(name.downcase) { return super })
|
100
|
+
end
|
101
|
+
|
102
|
+
def update_children_const(name, rest: false)
|
103
|
+
children_map = self::CHILDREN
|
104
|
+
already_has_rest = false
|
105
|
+
children_map.each do |key, value|
|
106
|
+
if value.is_a? Range
|
107
|
+
children_map[key] = children_map[key].begin..(children_map[key].end - 1)
|
108
|
+
already_has_rest = key
|
109
|
+
elsif value < 0
|
110
|
+
children_map[key] -= 1
|
111
|
+
end
|
112
|
+
end
|
113
|
+
children_map[name] = if rest
|
114
|
+
raise "Class #{self} can't have extra children '#{name}' because it already has '#{name}' (#{children_map.inspect})" if already_has_rest
|
115
|
+
children_map.size..-1
|
116
|
+
elsif already_has_rest
|
117
|
+
-1
|
118
|
+
else
|
119
|
+
children_map.size
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def define_accessor(name)
|
124
|
+
warn "child name '#{name}' conflicts with existing method for #{self}" if method_defined? name
|
125
|
+
class_eval <<-end_eval, __FILE__, __LINE__
|
126
|
+
def #{name}
|
127
|
+
children[#{name.upcase}]
|
128
|
+
end
|
129
|
+
end_eval
|
130
|
+
end
|
131
|
+
|
132
|
+
def add_runtime_check(name, type)
|
133
|
+
self::CHILDREN_TYPES[name] = type
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|