argtrace 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7398f53bb9b94105fc990221979803eaceebffa073e7487b16b558fc65755af6
4
+ data.tar.gz: 0744ce1ba76244daf8d442465e6ac7828cdf07b51f486b4b661c619f333419cb
5
+ SHA512:
6
+ metadata.gz: 274187675f07b8141b2ddbf05be0d344d5ce885cfc945c0d76912b2b77acfcba447eedaa1cc0dba0b671d1dee8b1dbe79fc4772b1747a8f946952a98749ce5f4
7
+ data.tar.gz: 3e05fa0e9067ec243aca02b4e94a016e9e193cd26d0bd321295dd1c8159926f7a48274eaee52d85121ca60eeb9d5b3dcfef7505f4ddc13fba74c6cd894c0f991
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ Gemfile.lock
10
+ /.vscode/
data/.rubocop.yml ADDED
@@ -0,0 +1,10 @@
1
+ Style/StringLiterals:
2
+ Enabled: true
3
+ EnforcedStyle: double_quotes
4
+
5
+ Style/StringLiteralsInInterpolation:
6
+ Enabled: true
7
+ EnforcedStyle: double_quotes
8
+
9
+ Layout/LineLength:
10
+ Max: 120
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in argtrace.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
11
+
12
+ gem "rubocop", "~> 0.80"
13
+
14
+ gem "ruby-debug-ide"
15
+ gem "debase"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Riki Ishikawa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # Argtrace
2
+
3
+ Argtrace is a Ruby MRI method type analyser.
4
+
5
+ Argtrace uses TracePoint and traces all of method calling,
6
+ peeks actual type of parameters and return value,
7
+ and finally summarize them into RBS format.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'argtrace'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle install
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install argtrace
24
+
25
+ ## Usage
26
+
27
+ ### 1. Explicit trace
28
+ ```ruby
29
+ require 'argtrace'
30
+ Argtrace::Default.main(rbs_path: "sig.rbs")
31
+
32
+ ... (YOUR PROGRAM HERE) ...
33
+ ```
34
+ RBS file is saved as "sig.rbs" in current directory.
35
+
36
+ ### 2. Implicit trace
37
+ ```console
38
+ $ ruby -r argtrace/autorun YOUR_PROGRAM_HERE.rb
39
+ ```
40
+ RBS file is saved as "sig.rbs" in current directory.
41
+
42
+ ### Restriction
43
+ Argtrace cannot work with C-extension,
44
+ because TracePoint doesn't provide feature to access arguments of calls into C-extension.
45
+
46
+
47
+ ## Contributing
48
+
49
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jljse/argtrace.
50
+
51
+ ## License
52
+
53
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
data/argtrace.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/argtrace/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "argtrace"
7
+ spec.version = Argtrace::VERSION
8
+ spec.authors = ["Riki Ishikawa"]
9
+ spec.email = ["riki.ishikawa@gmail.com"]
10
+
11
+ spec.summary = "trace arguments of method calls or block calls."
12
+ spec.description = "library to trace arguments of method calls or block calls with TracePoint and do callback."
13
+ spec.homepage = "https://github.com/jljse/argtrace/"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
16
+
17
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/jljse/argtrace/"
21
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ # Uncomment to register a new dependency of your gem
33
+ # spec.add_dependency "example-gem", "~> 1.0"
34
+
35
+ # For more information and examples about making a new gem, checkout our
36
+ # guide at: https://bundler.io/guides/creating_gem.html
37
+ end
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "argtrace"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/argtrace.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "argtrace/version"
4
+ require_relative "argtrace/signature.rb"
5
+ require_relative "argtrace/tracer.rb"
6
+ require_relative "argtrace/typelib.rb"
7
+ require_relative "argtrace/default.rb"
8
+
9
+ module Argtrace
10
+ class ArgtraceError < StandardError; end
11
+ end
12
+
@@ -0,0 +1,3 @@
1
+ require_relative '../argtrace.rb'
2
+
3
+ Argtrace::Default.main()
@@ -0,0 +1,32 @@
1
+ module Argtrace
2
+ # Default tracing setting.
3
+ class Default
4
+ # Default tracing setting. Analyse only user sources, and output them into RBS file.
5
+ def self.main(rbs_path: "sig.rbs")
6
+ typelib = Argtrace::TypeLib.new
7
+ tracer = Argtrace::Tracer.new
8
+
9
+ tracer.set_filter do |tp|
10
+ if [:call, :return].include?(tp.event)
11
+ tracer.user_source?(tp.defined_class, tp.method_id)
12
+ else
13
+ true
14
+ end
15
+ end
16
+
17
+ tracer.set_notify do |ev, callinfo|
18
+ if ev == :return
19
+ typelib.learn(callinfo.signature)
20
+ end
21
+ end
22
+
23
+ tracer.set_exit do
24
+ File.open(rbs_path, "w") do |f|
25
+ f.puts typelib.to_rbs
26
+ end
27
+ end
28
+
29
+ tracer.start_trace
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,233 @@
1
+
2
+ module Argtrace
3
+
4
+ # signature of method/block
5
+ class Signature
6
+ attr_accessor :defined_class, :method_id, :is_singleton_method, :params, :return_type
7
+
8
+ def initialize
9
+ @is_singleton_method = false
10
+ @params = []
11
+ end
12
+
13
+ def merge(all_params, ret)
14
+ unless @params
15
+ @params = []
16
+ end
17
+ # all params (including optional / keyword etc)
18
+ for i in 0...all_params.size
19
+ if i == @params.size
20
+ @params << all_params[i] # TODO: dup
21
+ else
22
+ if @params[i].mode == all_params[i].mode &&
23
+ @params[i].name == all_params[i].name
24
+ if all_params[i].mode == :block
25
+ # TODO: buggy
26
+ # merging of block parameter type is quite tricky...
27
+ @params[i].type.merge(
28
+ all_params[i].type.params, nil)
29
+ else
30
+ @params[i].type.merge_union(all_params[i].type)
31
+ end
32
+ else
33
+ raise "signature change not supported"
34
+ end
35
+ end
36
+ end
37
+
38
+ if ret
39
+ unless @return_type
40
+ @return_type = TypeUnion.new
41
+ end
42
+ @return_type.merge_union(ret)
43
+ end
44
+ end
45
+
46
+ def get_block_param
47
+ @params.find{|x| x.mode == :block}
48
+ end
49
+
50
+ def to_s
51
+ "Signature(#{@defined_class}::#{@method_id}(" + @params.map{|x| x.to_s}.join(",") + ") => #{@return_type.to_s})"
52
+ end
53
+
54
+ def inspect
55
+ to_s
56
+ end
57
+ end
58
+
59
+ # instance for one parameter
60
+ class Parameter
61
+ attr_accessor :mode, :name, :type
62
+
63
+ def hash
64
+ [@mode, @name, @type].hash
65
+ end
66
+
67
+ def to_s
68
+ "Parameter(#{@name}@#{@mode}:#{@type.to_s})"
69
+ end
70
+
71
+ def inspect
72
+ to_s
73
+ end
74
+ end
75
+
76
+ # Union of types (e.g. String | Integer)
77
+ class TypeUnion
78
+ attr_accessor :union;
79
+
80
+ def initialize
81
+ @union = []
82
+ end
83
+
84
+ def merge_union(other_union)
85
+ other_union.union.each do |type|
86
+ self.add(type)
87
+ end
88
+ end
89
+
90
+ def add(type)
91
+ for i in 0...@union.size
92
+ if @union[i] == type
93
+ # already in union
94
+ return
95
+ end
96
+ if @union[i].superclass_of?(type)
97
+ # already in union
98
+ return
99
+ end
100
+ if type.superclass_of?(@union[i])
101
+ # remove redundant element
102
+ @union[i] = nil
103
+ end
104
+ end
105
+ @union.compact!
106
+ @union << type
107
+ self
108
+ end
109
+
110
+ def to_s
111
+ if @union.empty?
112
+ "TypeUnion(None)"
113
+ else
114
+ "TypeUnion(" + @union.map{|x| x.to_s}.join("|") + ")"
115
+ end
116
+ end
117
+
118
+ def inspect
119
+ to_s
120
+ end
121
+ end
122
+
123
+ # placeholder for TrueClass / FalseClass
124
+ class BooleanClass
125
+ end
126
+
127
+ # type in RBS manner
128
+ class Type
129
+ attr_accessor :data, :subdata
130
+
131
+ def initialize()
132
+ @data = nil
133
+ @subdata = nil
134
+ end
135
+
136
+ def self.new_with_type(actual_type)
137
+ ret = Type.new
138
+ if actual_type == TrueClass || actual_type == FalseClass
139
+ ret.data = BooleanClass
140
+ else
141
+ ret.data = actual_type
142
+ end
143
+ return ret
144
+ end
145
+
146
+ def self.new_with_value(actual_value)
147
+ ret = Type.new
148
+ if actual_value.is_a?(Symbol)
149
+ # use symbol as type
150
+ ret.data = actual_value
151
+ elsif true == actual_value || false == actual_value
152
+ # warn: operands of == must in this order, because of override.
153
+ # treat true and false as boolean
154
+ ret.data = BooleanClass
155
+ elsif actual_value.class == Array
156
+ # TODO: multi type array
157
+ ret.data = Array
158
+ unless actual_value.empty?
159
+ if true == actual_value.first || false == actual_value.first
160
+ ret.subdata = BooleanClass
161
+ else
162
+ ret.subdata = actual_value.first.class
163
+ end
164
+ end
165
+ else
166
+ ret.data = actual_value.class
167
+ end
168
+ return ret
169
+ end
170
+
171
+ def hash
172
+ @data.hash
173
+ end
174
+
175
+ def ==(other)
176
+ if other.class != Type
177
+ return false
178
+ end
179
+ return @data == other.data && @subdata == other.subdata
180
+ end
181
+
182
+ def eql?(other)
183
+ self.==(other)
184
+ end
185
+
186
+ # true if self(Type) includes other(Type) as type declaration
187
+ # false if self and other is same Type.
188
+ def superclass_of?(other)
189
+ if other.class != Type
190
+ raise TypeError, "parameter must be Argtrace::Type"
191
+ end
192
+ if @data.is_a?(Symbol)
193
+ return false
194
+ elsif other.data.is_a?(Symbol)
195
+ return false
196
+ elsif @data == Array && other.data == Array
197
+ # TODO: merge for Array type like:
198
+ # Array[X] | Array[Y] => Array[X|Y]
199
+ if @subdata
200
+ if other.subdata
201
+ return other.subdata < @subdata
202
+ else
203
+ return true
204
+ end
205
+ else
206
+ # if self Array is untyped, cannot replace other as declaration.
207
+ return false
208
+ end
209
+ else
210
+ return other.data < @data
211
+ end
212
+ end
213
+
214
+ def to_s
215
+ if @data.is_a?(Symbol)
216
+ @data.inspect
217
+ elsif @data == Array
218
+ if @subdata
219
+ "Array[#{@subdata}]"
220
+ else
221
+ @data
222
+ end
223
+ else
224
+ @data.to_s
225
+ end
226
+ end
227
+
228
+ def inspect
229
+ to_s
230
+ end
231
+ end
232
+
233
+ end
@@ -0,0 +1,467 @@
1
+ require 'pathname'
2
+
3
+ module Argtrace
4
+
5
+ # instance per method/block call
6
+ class CallInfo
7
+ attr_accessor :signature
8
+
9
+ # actual block instance
10
+ attr_accessor :block_proc
11
+ end
12
+
13
+ # call stack of tracing targets
14
+ class CallStack
15
+ def initialize
16
+ # thread.object_id => stack
17
+ @stack = Hash.new{|h,k| h[k] = []}
18
+ end
19
+
20
+ def push_callstack(callinfo)
21
+ id = Thread.current.object_id
22
+ # DEBUG:
23
+ # p "[#{id}]>>" + " "*@stack[id].size*2 + callinfo.signature.to_s
24
+
25
+ @stack[id].push callinfo
26
+ end
27
+
28
+ def pop_callstack(tp)
29
+ id = Thread.current.object_id
30
+ ent = @stack[id].pop
31
+ if ent
32
+ # DEBUG:
33
+ # p "[#{id}]<<" + " "*@stack[id].size*2 + ent.signature.to_s
34
+
35
+ if tp.method_id != ent.signature.method_id
36
+ raise <<~EOF
37
+ callstack is broken
38
+ returning by tracepoint: #{tp.defined_class}::#{tp.method_id}
39
+ top of stack: #{ent.signature.to_s}
40
+ rest of stack:
41
+ #{@stack[id].map{|x| x.signature.to_s}.join("\n ")}
42
+ EOF
43
+ end
44
+ type = TypeUnion.new
45
+ type.add(Type.new_with_value(tp.return_value))
46
+ ent.signature.return_type = type
47
+ end
48
+ return ent
49
+ end
50
+
51
+ # find callinfo which use specific block
52
+ def find_by_block_location(path, lineno)
53
+ id = Thread.current.object_id
54
+ ret = []
55
+ @stack[id].each do |info|
56
+ if info.block_proc && info.block_proc.source_location == [path, lineno]
57
+ ret << info
58
+ end
59
+ end
60
+ return ret
61
+ end
62
+ end
63
+
64
+ # class definition related operation and result caching.
65
+ class DefinitionResolver
66
+ def initialize
67
+ # TODO:
68
+ end
69
+ end
70
+
71
+ # Main class for tracing with TracePoint.
72
+ class Tracer
73
+ attr_accessor :is_dead
74
+
75
+ def initialize()
76
+ @notify_block = nil
77
+ @callstack = CallStack.new
78
+ @tp_holder = nil
79
+ @is_dead = false
80
+
81
+ # prune_event_count > 0 while no need to notify.
82
+ # This is used to avoid undesirable signature lerning caused by error test.
83
+ @prune_event_count = 0
84
+
85
+ # cache of singleton-class => basic-class
86
+ @singleton_class_map_cache = {}
87
+
88
+ # cache of method location (klass => method_id => source_path)
89
+ @method_location_cache = Hash.new{|h, klass| h[klass] = {}}
90
+
91
+ # cache of judge result whether method is library-defined or user-defined
92
+ @ignore_paths_cache = {}
93
+ end
94
+
95
+ # entry point of trace event
96
+ def trace(tp)
97
+ if ignore_event?(tp)
98
+ return
99
+ end
100
+
101
+ if [:b_call, :b_return].include?(tp.event)
102
+ trace_block_event(tp)
103
+ else
104
+ trace_method_event(tp)
105
+ end
106
+ end
107
+
108
+ # process block call/return event
109
+ def trace_block_event(tp)
110
+ # I cannot determine the called block instance directly, so use block's location.
111
+ callinfos_with_block = @callstack.find_by_block_location(tp.path, tp.lineno)
112
+ callinfos_with_block.each do |callinfo|
113
+ block_param = callinfo.signature.get_block_param
114
+ block_param_types = get_param_types(callinfo.block_proc.parameters, tp)
115
+ # TODO: return type (but maybe, there is no demand)
116
+ block_param.type.merge(block_param_types, nil)
117
+ end
118
+ end
119
+
120
+ # process method call/return event
121
+ def trace_method_event(tp)
122
+ if [:call, :c_call].include?(tp.event)
123
+ # I don't know why but tp.parameters is different from called_method.parameters
124
+ # and called_method.parameters not work.
125
+ # called_method = get_called_method(tp)
126
+
127
+ case check_event_filter(tp)
128
+ when :prune
129
+ @prune_event_count += 1
130
+ skip_flag = true
131
+ when false
132
+ skip_flag = true
133
+ end
134
+
135
+ callinfo = CallInfo.new
136
+ signature = Signature.new
137
+ signature.defined_class = non_singleton_class(tp.defined_class)
138
+ signature.method_id = tp.method_id
139
+ signature.is_singleton_method = tp.defined_class.singleton_class?
140
+ signature.params = get_param_types(tp.parameters, tp)
141
+ callinfo.signature = signature
142
+ callinfo.block_proc = get_block_param_value(tp.parameters, tp)
143
+
144
+ @callstack.push_callstack(callinfo)
145
+
146
+ if !skip_flag && @prune_event_count == 0
147
+ # skip if it's object specific method
148
+ @notify_block.call(tp.event, callinfo) if @notify_block
149
+ end
150
+ else
151
+ case check_event_filter(tp)
152
+ when :prune
153
+ @prune_event_count -= 1
154
+ skip_flag = true
155
+ when false
156
+ skip_flag = true
157
+ end
158
+
159
+ callinfo = @callstack.pop_callstack(tp)
160
+ if callinfo
161
+ if !skip_flag && @prune_event_count == 0
162
+ @notify_block.call(tp.event, callinfo) if @notify_block
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ def standard_lib_root_path
169
+ # Search for standard lib path by some method location.
170
+ # I choose Pathname#parent here.
171
+ path = get_location(Pathname, :parent)
172
+ lib_dir = File.dirname(path)
173
+ return lib_dir
174
+ end
175
+
176
+ # true if method is defined in user source
177
+ def user_source?(klass, method_id)
178
+ path = get_location(klass, method_id)
179
+ return false unless path
180
+
181
+ unless @ignore_paths_cache.key?(path)
182
+ if path.start_with?("<internal:")
183
+ # skip all ruby internal method
184
+ @ignore_paths_cache[path] = true
185
+ elsif path == "(eval)"
186
+ # skip all eval
187
+ @ignore_paths_cache[path] = true
188
+ elsif path.start_with?(standard_lib_root_path())
189
+ # skip all standard lib
190
+ @ignore_paths_cache[path] = true
191
+ elsif Gem.path.any?{|x| path.start_with?(x)}
192
+ # skip all installed gem files
193
+ @ignore_paths_cache[path] = true
194
+ else
195
+ @ignore_paths_cache[path] = false
196
+ end
197
+ end
198
+ return ! @ignore_paths_cache[path]
199
+ end
200
+
201
+ def get_location(klass, method_id)
202
+ unless @method_location_cache[klass].key?(method_id)
203
+ path = nil
204
+ m = klass.instance_method(method_id)
205
+ if m and m.source_location
206
+ path = m.source_location[0]
207
+ end
208
+ @method_location_cache[klass][method_id] = path
209
+ end
210
+
211
+ return @method_location_cache[klass][method_id]
212
+ end
213
+
214
+ # true if klass is defined under Module
215
+ def under_module?(klass, mod)
216
+ ks = non_singleton_class(klass).to_s
217
+ ms = mod.to_s
218
+ return ks == ms || ks.start_with?(ms + "::")
219
+ end
220
+
221
+ # convert singleton class (like #<Class:Regexp>) to non singleton class (like Regexp)
222
+ def non_singleton_class(klass)
223
+ unless klass.singleton_class?
224
+ return klass
225
+ end
226
+
227
+ if /^#<Class:([A-Za-z0-9_:]+)>$/ =~ klass.inspect
228
+ # maybe normal name class
229
+ klass_name = Regexp.last_match[1]
230
+ begin
231
+ ret_klass = klass_name.split('::').inject(Kernel){|nm, sym| nm.const_get(sym)}
232
+ rescue => e
233
+ $stderr.puts "----- argtrace bug -----"
234
+ $stderr.puts "cannot convert class name #{klass} => #{klass_name}"
235
+ $stderr.puts e.full_message
236
+ $stderr.puts "------------------------"
237
+ raise
238
+ end
239
+ return ret_klass
240
+ end
241
+
242
+ # maybe this class is object's singleton class / special named class.
243
+ # I can't find efficient way, so cache the calculated result.
244
+ if @singleton_class_map_cache.key?(klass)
245
+ return @singleton_class_map_cache[klass]
246
+ end
247
+ begin
248
+ ret_klass = ObjectSpace.each_object(Module).find{|x| x.singleton_class == klass}
249
+ @singleton_class_map_cache[klass] = ret_klass
250
+ rescue => e
251
+ $stderr.puts "----- argtrace bug -----"
252
+ $stderr.puts "cannot convert class name #{klass} => #{klass_name}"
253
+ $stderr.puts e.full_message
254
+ $stderr.puts "------------------------"
255
+ raise
256
+ end
257
+ return ret_klass
258
+ end
259
+
260
+ # convert parameters to Parameter[]
261
+ def get_param_types(parameters, tp)
262
+ if tp.event == :c_call
263
+ # I cannot get parameter values of c_call ...
264
+ return []
265
+ else
266
+ return parameters.map{|param|
267
+ # param[0]=:req, param[1]=:x
268
+ p = Parameter.new
269
+ p.mode = param[0]
270
+ p.name = param[1]
271
+ if param[0] == :block
272
+ p.type = Signature.new
273
+ elsif param[1] == :* || param[1] == :&
274
+ # workaround for ActiveSupport gem.
275
+ # I don't know why this happen. just discard info about it.
276
+ type = TypeUnion.new
277
+ p.type = type
278
+ else
279
+ # TODO: this part is performance bottleneck caused by eval,
280
+ # but It's essential code
281
+ type = TypeUnion.new
282
+ begin
283
+ val = tp.binding.eval(param[1].to_s)
284
+ rescue => e
285
+ $stderr.puts "----- argtrace bug -----"
286
+ $stderr.puts parameters.inspect
287
+ $stderr.puts e.full_message
288
+ $stderr.puts "------------------------"
289
+ raise
290
+ end
291
+ type.add Type.new_with_value(val)
292
+ p.type = type
293
+ end
294
+ p
295
+ }
296
+ end
297
+ end
298
+
299
+ # pickup block parameter as proc if exists
300
+ def get_block_param_value(parameters, tp)
301
+ if tp.event == :c_call
302
+ # I cannot get parameter values of c_call ...
303
+ return nil
304
+ else
305
+ parameters.each do |param|
306
+ if param[0] == :block
307
+ if param[1] == :&
308
+ # workaround for ActiveSupport gem.
309
+ # I don't know why this happen. just discard info about it.
310
+ return nil
311
+ end
312
+ begin
313
+ val = tp.binding.eval(param[1].to_s)
314
+ rescue => e
315
+ $stderr.puts "----- argtrace bug -----"
316
+ $stderr.puts parameters.inspect
317
+ $stderr.puts e.full_message
318
+ $stderr.puts "------------------------"
319
+ raise
320
+ end
321
+ return val
322
+ end
323
+ end
324
+ return nil
325
+ end
326
+ end
327
+
328
+ # current called method object
329
+ def get_called_method(tp)
330
+ if tp.defined_class != tp.self.class
331
+ # I cannot identify all cases for this, so checks strictly.
332
+
333
+ if tp.defined_class.singleton_class?
334
+ # On class method call, "defined_class" becomes singleton(singular) class.
335
+ elsif tp.self.is_a?(tp.defined_class)
336
+ # On ancestor's method call, "defined_class" is different from self.class.
337
+ else
338
+ # This is unknown case.
339
+ raise "type inconsistent def:#{tp.defined_class} <=> self:#{tp.self.class} "
340
+ end
341
+ end
342
+ return tp.self.method(tp.method_id)
343
+ end
344
+
345
+ # true for the unhandleable events
346
+ def ignore_event?(tp)
347
+ if tp.defined_class.equal?(Class) and tp.method_id == :new
348
+ # On "Foo.new", I want "Foo" here,
349
+ # but "binding.receiver" equals to caller's "self" so I cannot get "Foo" from anywhere.
350
+ # Just ignore.
351
+ return true
352
+ end
353
+
354
+ if tp.defined_class.equal?(BasicObject) and tp.method_id == :initialize
355
+ # On "Foo#initialize", I want "Foo" here,
356
+ # but if "Foo" doesn't have explicit "initialize" method then no clue to get "Foo".
357
+ # Just ignore.
358
+ return true
359
+ end
360
+
361
+ if tp.defined_class.equal?(Class) and tp.method_id == :inherited
362
+ # I can't understand this.
363
+ # Just ignore.
364
+ return true
365
+ end
366
+
367
+ if tp.defined_class.equal?(Module) and tp.method_id == :method_added
368
+ # I can't understand this.
369
+ # Just ignore.
370
+ return true
371
+ end
372
+
373
+ return false
374
+ end
375
+
376
+ # check filter from set_filter
377
+ def check_event_filter(tp)
378
+ if @prune_event_filter
379
+ return @prune_event_filter.call(tp)
380
+ else
381
+ return true
382
+ end
383
+ end
384
+
385
+ # set event filter
386
+ # true = normal process
387
+ # false = skip notify
388
+ # :prune = skip notify and skip all nested events
389
+ def set_filter(&prune_event_filter)
390
+ @prune_event_filter = prune_event_filter
391
+ end
392
+
393
+ def set_exit(&exit_block)
394
+ @exit_block = exit_block
395
+ end
396
+
397
+ def set_notify(&notify_block)
398
+ @notify_block = notify_block
399
+ end
400
+
401
+ def call_exit
402
+ @exit_block.call if @exit_block
403
+ end
404
+
405
+ def enable
406
+ @tp_holder.enable
407
+ end
408
+
409
+ def disable
410
+ @tp_holder.disable
411
+ end
412
+
413
+ def stop_trace()
414
+ self.disable
415
+ Tracer.remove_running_trace(self)
416
+ end
417
+
418
+ # start TracePoint with callback block
419
+ def start_trace()
420
+ tp = TracePoint.new(:c_call, :c_return, :call, :return, :b_call) do |tp|
421
+ begin
422
+ tp.disable
423
+ # DEBUG:
424
+ # p [tp.event, tp.defined_class, tp.method_id]
425
+ self.trace(tp)
426
+ rescue => e
427
+ $stderr.puts "----- argtrace catch exception -----"
428
+ $stderr.puts e.full_message
429
+ $stderr.puts "------------------------------------"
430
+ @is_dead = true
431
+ ensure
432
+ tp.enable unless @is_dead
433
+ end
434
+ end
435
+ @tp_holder = tp
436
+
437
+ # hold reference and register at_exit
438
+ Tracer.add_running_trace(self)
439
+
440
+ tp.enable
441
+ end
442
+
443
+ @@running_trace_first = true
444
+ @@running_trace = []
445
+ def self.add_running_trace(trace)
446
+ @@running_trace << trace
447
+ if @@running_trace_first
448
+ @@running_trace_first = false
449
+ at_exit do
450
+ @@running_trace.each do |trace|
451
+ trace.disable
452
+ end
453
+ @@running_trace.each do |trace|
454
+ trace.call_exit
455
+ end
456
+ @@running_trace.clear
457
+ end
458
+ end
459
+ end
460
+
461
+ def self.remove_running_trace(trace)
462
+ @@running_trace.delete(trace)
463
+ end
464
+
465
+ end
466
+
467
+ end
@@ -0,0 +1,230 @@
1
+ require 'set'
2
+
3
+ module Argtrace
4
+
5
+ # Store of signatures
6
+ class TypeLib
7
+ # class => { method_id => [normal_method_signature, singleton_method_signature] }
8
+ def lib
9
+ @lib
10
+ end
11
+
12
+ def initialize
13
+ @lib = Hash.new{|hklass, klass|
14
+ hklass[klass] = Hash.new{|hmethod, method_id|
15
+ hmethod[method_id] = [nil, nil]
16
+ }
17
+ }
18
+ end
19
+
20
+ def ready_signature(signature)
21
+ pair = @lib[signature.defined_class][signature.method_id]
22
+ index = signature.is_singleton_method ? 1 : 0
23
+ unless pair[index]
24
+ sig = Signature.new
25
+ sig.defined_class = signature.defined_class
26
+ sig.method_id = signature.method_id
27
+ sig.is_singleton_method = signature.is_singleton_method
28
+ sig.return_type = nil
29
+ pair[index] = sig
30
+ end
31
+ return pair[index]
32
+ end
33
+
34
+ def learn(signature)
35
+ ready_signature(signature).merge(signature.params, signature.return_type)
36
+ end
37
+
38
+ def to_rbs
39
+ # TODO: should I output class inheritance info ?
40
+ # TODO: private/public
41
+ # TODO: attr_reader/attr_writer/attr_accessor
42
+ mod_root = OutputModule.new
43
+
44
+ @lib.keys.sort_by{|x| x.to_s}.each do |klass|
45
+ klass_methods = @lib[klass]
46
+
47
+ # output instance method first, and then output singleton method.
48
+ [0, 1].each do |instance_or_singleton|
49
+ klass_methods.keys.sort.each do |method_id|
50
+ sig = klass_methods[method_id][instance_or_singleton]
51
+ next unless sig
52
+
53
+ mod_root.add_signature(sig)
54
+ end
55
+ end
56
+ end
57
+
58
+ mod_root.to_rbs
59
+ end
60
+ end
61
+
62
+ # helper to convert TypeLib into RBS. OutputMoudle acts like Module tree node.
63
+ class OutputModule
64
+ attr_accessor :actual_module, :name, :children, :signatures
65
+
66
+ def initialize
67
+ @children = {}
68
+ @signatures = []
69
+ end
70
+
71
+ def add_signature(signature)
72
+ # this is root node, so use Kernel as const resolve source.
73
+ @actual_module = Kernel
74
+
75
+ constname = class_const_name(signature.defined_class)
76
+ unless constname
77
+ # cannot handle this
78
+ return
79
+ end
80
+
81
+ add_signature_inner(constname, signature)
82
+ end
83
+
84
+ # split class name into consts (e.g. Argtrace::TypeLib to ["Argtrace", "TypeLib"])
85
+ def class_const_name(klass)
86
+ if /^[A-Za-z0-9_:]+$/ =~ klass.to_s
87
+ # this should be normal name
88
+ consts = klass.to_s.split("::")
89
+
90
+ # assertion
91
+ resolved_class = consts.inject(Kernel){|mod, const| mod.const_get(const)}
92
+ if klass != resolved_class
93
+ $stderr.puts "----- argtrace bug -----"
94
+ $stderr.puts "#{klass} => #{consts} => #{resolved_class}"
95
+ $stderr.puts "------------------------"
96
+ raise "Failed to resolve class by constant"
97
+ end
98
+
99
+ return consts
100
+ else
101
+ return nil
102
+ end
103
+ end
104
+
105
+ def add_signature_inner(name_consts, signature)
106
+ if name_consts.empty?
107
+ @signatures << signature
108
+ else
109
+ unless @children.key?(name_consts.first)
110
+ mod = OutputModule.new
111
+ mod.name = name_consts.first
112
+ mod.actual_module = @actual_module.const_get(name_consts.first)
113
+ @children[name_consts.first] = mod
114
+ end
115
+ current_resolving_name = name_consts.shift
116
+ @children[current_resolving_name].add_signature_inner(name_consts, signature)
117
+ end
118
+ end
119
+
120
+ def to_rbs
121
+ # this is root node
122
+ lines = []
123
+ @children.keys.sort.each do |child_name|
124
+ lines << @children[child_name].to_rbs_inner(0)
125
+ lines << ""
126
+ end
127
+ return lines.join("\n")
128
+ end
129
+
130
+ def to_rbs_inner(indent_level)
131
+ indent = " " * indent_level
132
+ classmod_def = @actual_module.class == Class ? "class" : "module"
133
+
134
+ lines = []
135
+ lines << "#{indent}#{classmod_def} #{name}"
136
+ @children.keys.sort.each do |child_name|
137
+ lines << @children[child_name].to_rbs_inner(indent_level + 1)
138
+ lines << ""
139
+ end
140
+ @signatures.each do |sig|
141
+ lines << sig_to_rbs(indent_level + 1, sig)
142
+ end
143
+ lines << "#{indent}end"
144
+ return lines.join("\n")
145
+ end
146
+
147
+ def sig_to_rbs(indent_level, signature)
148
+ indent = " " * indent_level
149
+ sig_name = signature.is_singleton_method ? "self.#{signature.method_id}" : signature.method_id
150
+ params = signature.params
151
+ .filter{|p| p.mode != :block}
152
+ .map{|p| param_to_rbs(p)}
153
+ .compact
154
+ .join(", ")
155
+ rettype = type_union_to_rbs(signature.return_type)
156
+ blocktype = blocktype_to_rbs(signature.params.find{|p| p.mode == :block})
157
+ return "#{indent}def #{sig_name} : (#{params})#{blocktype} -> #{rettype}"
158
+ end
159
+
160
+ def blocktype_to_rbs(blockparam)
161
+ unless blockparam
162
+ return ""
163
+ end
164
+ params = blockparam.type.params
165
+ .map{|p| type_union_to_rbs(p.type)}
166
+ .join(", ")
167
+ return " { (#{params}) -> untyped }"
168
+ end
169
+
170
+ def param_to_rbs(param)
171
+ case param.mode
172
+ when :req
173
+ return "#{type_union_to_rbs(param.type)} #{param.name}"
174
+ when :opt
175
+ return "?#{type_union_to_rbs(param.type)} #{param.name}"
176
+ when :keyreq
177
+ return "#{param.name}: #{type_union_to_rbs(param.type)}"
178
+ when :key
179
+ return "?#{param.name}: #{type_union_to_rbs(param.type)}"
180
+ when :block
181
+ return nil
182
+ end
183
+ end
184
+
185
+ def type_union_to_rbs(typeunion)
186
+ if typeunion.union.size == 0
187
+ return "untyped"
188
+ end
189
+ # TODO: ugly
190
+ if typeunion.union.size == 1 and NilClass == typeunion.union.first.data
191
+ # TODO: I can't distinguish nil and untyped.
192
+ return "untyped"
193
+ end
194
+ if typeunion.union.size == 2 and typeunion.union.any?{|x| NilClass == x.data}
195
+ # type is nil and sometype, so represent it as "sometype?"
196
+ sometype = typeunion.union.find{|x| NilClass != x.data}
197
+ return "#{type_to_rbs(sometype)}?"
198
+ end
199
+
200
+ ret = typeunion.union.map{|type| type_to_rbs(type)}.join("|")
201
+ return ret
202
+ end
203
+
204
+ def type_to_rbs(type)
205
+ if type.data.is_a?(Symbol)
206
+ return type.data.inspect
207
+ elsif true == type.data || false == type.data || BooleanClass == type.data
208
+ return "bool"
209
+ elsif nil == type.data || NilClass == type.data
210
+ return "nil"
211
+ elsif Array == type.data
212
+ if type.subdata
213
+ case type.subdata
214
+ when true, false, BooleanClass
215
+ elementtype = "bool"
216
+ else
217
+ elementtype = type.subdata.to_s
218
+ end
219
+ return "Array[#{elementtype}]"
220
+ else
221
+ return "Array"
222
+ end
223
+ else
224
+ return type.data.to_s
225
+ end
226
+ end
227
+
228
+ end
229
+ end
230
+
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Argtrace
4
+ VERSION = "0.1.1"
5
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: argtrace
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Riki Ishikawa
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-03-14 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: library to trace arguments of method calls or block calls with TracePoint
14
+ and do callback.
15
+ email:
16
+ - riki.ishikawa@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".gitignore"
22
+ - ".rubocop.yml"
23
+ - Gemfile
24
+ - LICENSE.txt
25
+ - README.md
26
+ - Rakefile
27
+ - argtrace.gemspec
28
+ - bin/console
29
+ - bin/setup
30
+ - lib/argtrace.rb
31
+ - lib/argtrace/autorun.rb
32
+ - lib/argtrace/default.rb
33
+ - lib/argtrace/signature.rb
34
+ - lib/argtrace/tracer.rb
35
+ - lib/argtrace/typelib.rb
36
+ - lib/argtrace/version.rb
37
+ homepage: https://github.com/jljse/argtrace/
38
+ licenses:
39
+ - MIT
40
+ metadata:
41
+ homepage_uri: https://github.com/jljse/argtrace/
42
+ source_code_uri: https://github.com/jljse/argtrace/
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 2.7.0
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.2.3
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: trace arguments of method calls or block calls.
62
+ test_files: []