deep-cover 0.1.1
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.
- 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
|