type_tracer 0.1.0
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 +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +8 -0
- data/.travis.yml +7 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +197 -0
- data/Rakefile +7 -0
- data/bin/console +15 -0
- data/bin/rspec +17 -0
- data/bin/setup +8 -0
- data/lib/type_tracer/arg_send_type_check/ast_checker.rb +44 -0
- data/lib/type_tracer/arg_send_type_check/method_checker.rb +61 -0
- data/lib/type_tracer/arg_send_type_check/runner.rb +54 -0
- data/lib/type_tracer/config.rb +22 -0
- data/lib/type_tracer/instance_method_checker.rb +52 -0
- data/lib/type_tracer/method_analyzer.rb +81 -0
- data/lib/type_tracer/parser.rb +21 -0
- data/lib/type_tracer/rack/type_sampler_middleware.rb +18 -0
- data/lib/type_tracer/rails/railtie.rb +14 -0
- data/lib/type_tracer/rake/tasks.rb +43 -0
- data/lib/type_tracer/real_arg_sends_checker.rb +62 -0
- data/lib/type_tracer/rspec/instance_double_arg_checker.rb +25 -0
- data/lib/type_tracer/sends_watcher.rb +12 -0
- data/lib/type_tracer/type_fetcher.rb +26 -0
- data/lib/type_tracer/type_sampler.rb +114 -0
- data/lib/type_tracer/version.rb +4 -0
- data/lib/type_tracer.rb +24 -0
- data/type_tracer.gemspec +33 -0
- metadata +191 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5e49de66db3b5881bb0f7b967e38c6b5f629de42
|
4
|
+
data.tar.gz: aad4ad204a948e8dc7463a89d9b3325e52b2de73
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 818a27edfff25e29d7773ac7784afddd9deda31e48a803ea57d35b670a18cf8a2799466fef4a7187493fa82c353f976ab5248de8a38dc2be75de0ca31ff59f8a
|
7
|
+
data.tar.gz: cc2d23289aacd3365347acdd88ac8fe8f856b8f152fa05f076df4374ca06081265aee0b7afcc6c3e278f83751a3a07e5d165f7247b8fe87cb2d1bf6c1d34f769
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 draffensperger
|
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,197 @@
|
|
1
|
+
# TypeTracer
|
2
|
+
|
3
|
+
[](https://travis-ci.org/draffensperger/type_tracer) [](https://codeclimate.com/github/draffensperger/type_tracer)
|
4
|
+
|
5
|
+
TypeTracer collects a set of experimental approaches to checking Ruby
|
6
|
+
"types" (particularly method signatures) while still allowing a decent level of
|
7
|
+
metaprogramming and without needing to specify annotations in the production
|
8
|
+
code.
|
9
|
+
|
10
|
+
It includes three proof-of-concept approaches to catch some `NoMethodError`
|
11
|
+
cases automatically.
|
12
|
+
|
13
|
+
See [tt_demos](https://github.com/draffensperger/tt_demos) for an example of using
|
14
|
+
approach 1.,
|
15
|
+
and [quote-randomizer](https://github.com/draffensperger/quote-randomizer)
|
16
|
+
for a simple Rails app that incorporates support for approaches 2. and 3.
|
17
|
+
|
18
|
+
### 1. Enhancing RSpec `instance_double` to check stubbed argument values
|
19
|
+
|
20
|
+
Even if you have 100% unit test coverage, there still can be invalid method
|
21
|
+
calls in the "seams" between your classes or groups of classes.
|
22
|
+
|
23
|
+
RSpec's [Verifying Doubles](https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles),
|
24
|
+
such as `instance_double` will check that stubbed method calls are actually
|
25
|
+
defined on a instance of the class you specify and that the method arity (number
|
26
|
+
of arguments) matches what you are stubbing.
|
27
|
+
|
28
|
+
This gem includes a file you can include which monkey-patches `instance_double`
|
29
|
+
to parse the abstract syntax tree of the stubbed method to check whether the
|
30
|
+
arguments to the stubbed call would result in an `NoMethodError` were they actually to be passed into the stubbed method.
|
31
|
+
|
32
|
+
To make your `instance_double`'s behave like that, include `type_tracer` in your
|
33
|
+
`Gemfile` and add `require 'type_tracer/rspec/Instance_double_arg_checker`.
|
34
|
+
|
35
|
+
It can only reliably assume that a method will be called on the actual value of
|
36
|
+
an argument if that argument variable isn't reassigned and if the method doesn't
|
37
|
+
contain any branches (because it could branch on the type of the argument). A
|
38
|
+
lot of methods contain branches, so I may try to tighten that requirement (e.g.
|
39
|
+
that the expression that it branches on must involve the argument).
|
40
|
+
|
41
|
+
### 2. Check instance method calls in app environment (for runtime defined methods)
|
42
|
+
|
43
|
+
It's very difficult / impossible to simply statically analyze Ruby code for
|
44
|
+
undefined methods (though see the
|
45
|
+
[ruby-lint](https://github.com/YorickPeterse/ruby-lint) gem for something that
|
46
|
+
does a decent job at it).
|
47
|
+
|
48
|
+
The approach here is instead to combine static and dynamic analysis of a Ruby program.
|
49
|
+
That is to run the "undefined method" analysis in the context of a fully loaded
|
50
|
+
app environment where many/most of the dynamically defined methods have been
|
51
|
+
defined. In a typical Rails app for instance, the `has_many` association methods
|
52
|
+
would be defined when a class is loaded.
|
53
|
+
|
54
|
+
However, the database attribute methods for `ActiveRecord` objects won't be
|
55
|
+
defined until a request on that class is made e.g. from a call to `new`. My idea
|
56
|
+
would be for `TypeTracer` to provide a config option for a block that could be
|
57
|
+
called to induce most of the dynamically defined methods to get defined, e.g. by
|
58
|
+
calling `new` on all of the `ActiveRecord::Base` subclasses and perhaps doing
|
59
|
+
other application-specific method defining. Of course, not all dynamic methods
|
60
|
+
are defined in easy-to-induce ways, but this would still catch a lot of them.
|
61
|
+
|
62
|
+
To use it, include the `type_tracer` gem in your `Gemfile`, then, assuming
|
63
|
+
you're running a Rails app, you can run a new Rake task
|
64
|
+
`rake type_tracer:check_method_calls` that will check for undefined top-level
|
65
|
+
instance method calls (i.e. calls on `self` in methods).
|
66
|
+
|
67
|
+
It's possible that you will have methods that are defined not at the time that a
|
68
|
+
class is first loaded but later in the app runtime. E.g. ActiveRecord attribute
|
69
|
+
methods aren't defined until the first ActiveRecord operation for that model.
|
70
|
+
|
71
|
+
What `type_tracer` provides is a config option called `attribute_methods_definer`
|
72
|
+
that specifies a proc that is called after the app environment is loaded but
|
73
|
+
before the analysis starts. Here's an example that will call `new` on all
|
74
|
+
classes that inherit from `ActiveRecord::Base` which will have the effect of
|
75
|
+
making those classes define their attribute methods.
|
76
|
+
|
77
|
+
```
|
78
|
+
# config/initializers/type_tracer.rb
|
79
|
+
require 'type_tracer'
|
80
|
+
|
81
|
+
TypeTracer.config do |config|
|
82
|
+
config.attribute_methods_definer = proc do
|
83
|
+
# initialize all of the active record models so that they will define their
|
84
|
+
# attribute methods.
|
85
|
+
ActiveRecord::Base.descendants.each(&:new)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
This is similar to how you would define attribute methods for a dynamic class if
|
91
|
+
you are using RSpec verifying doubles, see their doc for
|
92
|
+
[dynamic-classes](https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles/dynamic-classes)
|
93
|
+
|
94
|
+
### 3A. Sampling implied types from production to use in further analysis
|
95
|
+
|
96
|
+
This is not a direct way to catch bad method calls, but it's an idea to use the
|
97
|
+
running production application to gather the real implicit type signature for
|
98
|
+
methods and then feed that back into the specs or analysis tools as more
|
99
|
+
realistic type information (without needing to annotate production code or be
|
100
|
+
super-specific about what the types actually are).
|
101
|
+
|
102
|
+
It's common, for instance, for a method to take either `nil` or a specific value.
|
103
|
+
It's also decently comment for a method to take a "duck type" i.e. it could take
|
104
|
+
instances of a range of classes but call a fixed set of methods on all of them.
|
105
|
+
Sometimes too a method may have an explicit `is_a?` check and do different
|
106
|
+
things for different types (e.g. a method that operates recursively on a nested
|
107
|
+
structure that could be either an Array or Hash).
|
108
|
+
|
109
|
+
To try to represent as many of those cases as possible, the traced types are in
|
110
|
+
effect union types of all the different classes that are passed in and what
|
111
|
+
methods are called on instances of those different classes.
|
112
|
+
|
113
|
+
To use type tracing in your Rails app, include the `type_tracer` gem, and then
|
114
|
+
add an initializer like this:
|
115
|
+
|
116
|
+
```
|
117
|
+
# config/initializers/type_tracer.rb
|
118
|
+
require 'type_tracer'
|
119
|
+
|
120
|
+
TypeTracer.config do |config|
|
121
|
+
# This configures an Rack middleware for what requests to type sample on
|
122
|
+
config.sample_types_for_requests do |_rack_env|
|
123
|
+
# Only type sample 1% of requests
|
124
|
+
rand() > 0.01
|
125
|
+
end
|
126
|
+
|
127
|
+
# These configure the files to do type sampling on and the remote URL of the
|
128
|
+
# sampled types endpoint in a deployed app.
|
129
|
+
config.type_check_root_path = Rails.root
|
130
|
+
config.type_check_path_regex = %r{\A(app|lib)/}
|
131
|
+
config.sampled_types_url = 'https://quote-randomizer.herokuapp.com/sampled_types'
|
132
|
+
|
133
|
+
# To make the sampled types useful for checking local changes, we need to be
|
134
|
+
# able to know the git commit that produced the sampled types in case they are
|
135
|
+
# no longer applicable given local changes (if you changed the callers of the
|
136
|
+
# method in question).
|
137
|
+
# This will give the git commit on Heroku, though requires this buildpack:
|
138
|
+
# https://github.com/ianpurvis/heroku-buildpack-version
|
139
|
+
config.git_commit = ENV['SOURCE_VERSION']
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
You'll also need to set up an endpoint in your app that will serve the sampled
|
144
|
+
types (as referenced above with the `sampled_types_url`). Here's an example
|
145
|
+
controller (that would need a corresponding route as well):
|
146
|
+
|
147
|
+
```
|
148
|
+
class SampledTypesController < ApplicationController
|
149
|
+
def show
|
150
|
+
render json: JSON.pretty_generate(TypeTracer::TypeSampler.sampled_type_info)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
### 3B. Using sampled types to check local changes to a method for undefined method calls on arguments
|
156
|
+
|
157
|
+
To use the sampled method type signatures from a deployed app, run
|
158
|
+
`rake type_tracer:check_arg_sends`. That will fetch the sampled types and Git
|
159
|
+
commit hash from your specified `sampled_types_url`. It assumes you are using
|
160
|
+
Git for your project and have the `git` executable installed.
|
161
|
+
|
162
|
+
A similar caveat to the expanded `instance_double` above holds, that the
|
163
|
+
checking will assume the method is correct if it either contains branches or a
|
164
|
+
re-assignment of the argument variable (because those cases are harder to
|
165
|
+
analyze and a common idiom is e.g. `return if x.nil?` or `x ||= default_value`
|
166
|
+
both of which could change the type of the `x` argument).
|
167
|
+
|
168
|
+
## Installation
|
169
|
+
|
170
|
+
Add this line to your application's Gemfile:
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
gem 'type_tracer'
|
174
|
+
```
|
175
|
+
|
176
|
+
And then execute:
|
177
|
+
|
178
|
+
$ bundle
|
179
|
+
|
180
|
+
Or install it yourself as:
|
181
|
+
|
182
|
+
$ gem install type_tracer
|
183
|
+
|
184
|
+
## Development
|
185
|
+
|
186
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
187
|
+
|
188
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
189
|
+
|
190
|
+
## Contributing
|
191
|
+
|
192
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/type_tracer.
|
193
|
+
|
194
|
+
## License
|
195
|
+
|
196
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
197
|
+
|
data/Rakefile
ADDED
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 'type_tracer'
|
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
|
data/bin/rspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
#
|
4
|
+
# This file was generated by Bundler.
|
5
|
+
#
|
6
|
+
# The application 'rspec' is installed as part of a gem, and
|
7
|
+
# this file is here to facilitate running it.
|
8
|
+
#
|
9
|
+
|
10
|
+
require 'pathname'
|
11
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
|
12
|
+
Pathname.new(__FILE__).realpath)
|
13
|
+
|
14
|
+
require 'rubygems'
|
15
|
+
require 'bundler/setup'
|
16
|
+
|
17
|
+
load Gem.bin_path('rspec-core', 'rspec')
|
data/bin/setup
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypeTracer
|
4
|
+
module ArgSendTypeCheck
|
5
|
+
class AstChecker
|
6
|
+
def initialize(ast:, types:)
|
7
|
+
@ast = ast
|
8
|
+
@types = types
|
9
|
+
end
|
10
|
+
|
11
|
+
def bad_arg_sends
|
12
|
+
method_defs.flat_map(&method(:method_bad_arg_sends))
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def method_defs
|
18
|
+
@ast.each_descendant.select(&:def_type?)
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_bad_arg_sends(method_def)
|
22
|
+
class_sym = method_class_sym(method_def)
|
23
|
+
method_sym = method_def.children.first
|
24
|
+
|
25
|
+
return [] unless @types && @types[class_sym] &&
|
26
|
+
@types[class_sym][method_sym]
|
27
|
+
|
28
|
+
arg_types = @types[class_sym][method_sym][:arg_types]
|
29
|
+
MethodChecker.new(method_def: method_def, method_sym: method_sym,
|
30
|
+
class_sym: class_sym, arg_types: arg_types)
|
31
|
+
.bad_arg_send_messages
|
32
|
+
end
|
33
|
+
|
34
|
+
def method_class_sym(method_def)
|
35
|
+
context_ancestors = method_def.each_ancestor.select do |ancestor|
|
36
|
+
ancestor.class_type? || ancestor.module_type?
|
37
|
+
end
|
38
|
+
const_ancestors = context_ancestors.map { |n| n.children.first }
|
39
|
+
const_path = const_ancestors.map { |n| n.children[1] }
|
40
|
+
const_path.join('::').to_sym
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module TypeTracer
|
3
|
+
module ArgSendTypeCheck
|
4
|
+
class MethodChecker
|
5
|
+
def initialize(method_def:, method_sym:, class_sym:, arg_types:)
|
6
|
+
@method_def = method_def
|
7
|
+
@method_sym = method_sym
|
8
|
+
@class_sym = class_sym
|
9
|
+
@arg_types = arg_types
|
10
|
+
end
|
11
|
+
|
12
|
+
def bad_arg_send_messages
|
13
|
+
# For each argument to the method
|
14
|
+
analyzer.arg_names.flat_map(&method(:bad_arg_sends)).compact
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def bad_arg_sends(arg)
|
20
|
+
# For each of the sampled type classes for that argument
|
21
|
+
@arg_types[arg].flat_map do |arg_type|
|
22
|
+
bad_arg_sends_for_type(arg, arg_type[0])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def bad_arg_sends_for_type(arg, arg_type)
|
27
|
+
# Look up the sampled type class. We can do that because this runs with
|
28
|
+
# the app environment loaded into it.
|
29
|
+
type_class = Object.const_get(arg_type)
|
30
|
+
|
31
|
+
# For each of the statically-analyzed send calls on that argument
|
32
|
+
analyzer.arg_sends[arg].flat_map do |arg_send|
|
33
|
+
# Check to see if the send call (in a locally changed method)
|
34
|
+
# would be invalid based on the sampled type information (from the
|
35
|
+
# method as it is deployed currently).
|
36
|
+
next if instance_method?(type_class, arg_send)
|
37
|
+
bad_arg_send_message(arg, type_class, arg_send)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def analyzer
|
42
|
+
@analyzer ||= MethodAnalyzer.new(method_def: @method_def)
|
43
|
+
end
|
44
|
+
|
45
|
+
def bad_arg_send_message(arg_name, arg_type, arg_send)
|
46
|
+
source = @method_def.source_range
|
47
|
+
"The method #{@class_sym}##{@method_sym} as type sampled may receive a "\
|
48
|
+
"value of type #{arg_type} for the argument '#{arg_name}'. "\
|
49
|
+
"However, that type (#{arg_type}) does not contain the instance "\
|
50
|
+
"method '#{arg_send}' that the method tries to call on it. \n"\
|
51
|
+
'Method location:'\
|
52
|
+
"\n #{source.source_buffer.name}:#{source.line}\n"
|
53
|
+
end
|
54
|
+
|
55
|
+
def instance_method?(klass, symbol)
|
56
|
+
klass.instance_methods.include?(symbol) ||
|
57
|
+
klass.private_instance_methods.include?(symbol)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TypeTracer
|
4
|
+
module ArgSendTypeCheck
|
5
|
+
class Runner
|
6
|
+
def initialize(files)
|
7
|
+
@files = files
|
8
|
+
end
|
9
|
+
|
10
|
+
def bad_arg_sends
|
11
|
+
@files.flat_map do |file|
|
12
|
+
ast = TypeTracer.parse_file(file)
|
13
|
+
AstChecker.new(ast: ast, types: filtered_types).bad_arg_sends
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def changed_files
|
20
|
+
@changed_files ||=
|
21
|
+
`git diff --name-only #{fetched_types[:git_commit]}`.split("\n")
|
22
|
+
end
|
23
|
+
|
24
|
+
def fetched_types
|
25
|
+
@fetched_types ||= TypeFetcher.fetch_sampled_types
|
26
|
+
end
|
27
|
+
|
28
|
+
def filtered_types
|
29
|
+
@filtered_types ||= filter_types
|
30
|
+
end
|
31
|
+
|
32
|
+
def filter_types
|
33
|
+
type_info = fetched_types[:type_info]
|
34
|
+
type_info.each do |klass_sym, method_types|
|
35
|
+
method_types.each do |method_sym, method_type_info|
|
36
|
+
# All the callers of this method have at least one file changed, so
|
37
|
+
# assume that this method type signature may no longer be valid.
|
38
|
+
if all_have_changed_file(method_type_info[:callers])
|
39
|
+
# Remove it from the type list
|
40
|
+
type_info[klass_sym].delete(method_sym)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def all_have_changed_file(call_stacks)
|
47
|
+
call_stacks.all? do |call_stack|
|
48
|
+
stack_files = call_stack.map { |frame| frame.split(':')[0] }
|
49
|
+
!(stack_files & changed_files).empty?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'singleton'
|
3
|
+
|
4
|
+
module TypeTracer
|
5
|
+
class Config
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
attr_accessor :attribute_methods_definer, :type_check_root_path,
|
9
|
+
:type_check_path_regex, :git_commit, :sampled_types_url
|
10
|
+
|
11
|
+
# Set this by giving a block to sample_types_for_requests
|
12
|
+
attr_reader :rack_type_sample_decider
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@type_sampler_root_path = Dir.pwd
|
16
|
+
end
|
17
|
+
|
18
|
+
def sample_types_for_requests(&block)
|
19
|
+
@rack_type_sample_decider = block.to_proc
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'type_tracer/parser'
|
3
|
+
|
4
|
+
module TypeTracer
|
5
|
+
class InstanceMethodChecker
|
6
|
+
def initialize(ast)
|
7
|
+
@ast = ast
|
8
|
+
end
|
9
|
+
|
10
|
+
def undefined_method_messages
|
11
|
+
self_sends.map(&method(:bad_self_send_messages)).compact
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def self_sends
|
17
|
+
@ast.each_descendant.select do |node|
|
18
|
+
node.send_type? && node.children.first.nil?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def bad_self_send_messages(send_node)
|
23
|
+
symbol = send_node.children[1]
|
24
|
+
|
25
|
+
# private/protected specifiers are parsed as a send, so just ignore them
|
26
|
+
# define_method also doesn't get listed as a method but is one!
|
27
|
+
return if [:private, :protected, :define_method].include?(symbol)
|
28
|
+
|
29
|
+
# Look up the class for the AST send node in the loaded Ruby
|
30
|
+
# environment. We can do that because this is being run in a Rake
|
31
|
+
# rask with the loaded app environment.
|
32
|
+
klass = loaded_class(send_node)
|
33
|
+
return if klass.instance_methods.include?(symbol) ||
|
34
|
+
klass.private_instance_methods.include?(symbol) ||
|
35
|
+
klass.respond_to?(symbol)
|
36
|
+
|
37
|
+
# The abstract syntax tree keeps a reference to the node's file/line
|
38
|
+
source = send_node.source_range
|
39
|
+
"Likely undefined method: #{symbol} for #{klass} instance"\
|
40
|
+
"\n in #{source.source_buffer.name}:#{source.line}\n\n"
|
41
|
+
end
|
42
|
+
|
43
|
+
def loaded_class(send_node)
|
44
|
+
context_ancestors = send_node.each_ancestor.select do |ancestor|
|
45
|
+
ancestor.class_type? || ancestor.module_type?
|
46
|
+
end
|
47
|
+
const_ancestors = context_ancestors.map { |n| n.children.first }
|
48
|
+
const_path = const_ancestors.map { |n| n.children[1] }
|
49
|
+
Object.const_get(const_path.join('::'))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module TypeTracer
|
3
|
+
class MethodAnalyzer
|
4
|
+
def initialize(method_def:)
|
5
|
+
@method_def = method_def
|
6
|
+
end
|
7
|
+
|
8
|
+
def arg_names
|
9
|
+
@arg_names ||= args.children.map { |arg_node| arg_node.children.first }
|
10
|
+
end
|
11
|
+
|
12
|
+
def arg_sends
|
13
|
+
return @arg_sends if @arg_sends
|
14
|
+
# For the simple version, just assume we don't know what will happen if
|
15
|
+
# there are branches in the method, so assume that no args definitely get
|
16
|
+
# called (as there could be conditioning on arg type).
|
17
|
+
@arg_sends =
|
18
|
+
if branches?
|
19
|
+
empty_arg_sends
|
20
|
+
else
|
21
|
+
Hash[arg_names.map { |arg| [arg, sends_for_arg(arg)] }]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def branches?
|
28
|
+
@method_def.each_descendant.any? { |node| node.if_type? || node.case_type? }
|
29
|
+
end
|
30
|
+
|
31
|
+
def empty_arg_sends
|
32
|
+
Hash[arg_names.map { |n| [n, []] }]
|
33
|
+
end
|
34
|
+
|
35
|
+
def sends_for_arg(arg)
|
36
|
+
# Assume that if there are any assignments to the local variable for the
|
37
|
+
# argument that we can't confidently know the type of the argument anymore
|
38
|
+
# even if we know its type coming in, so don't assume we know what sends
|
39
|
+
# are made on the initial argument value itself.
|
40
|
+
if local_var_assigns.include?(arg)
|
41
|
+
[]
|
42
|
+
else
|
43
|
+
local_var_sends[arg].to_a
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def args
|
48
|
+
@method_def.each_descendant.find(&:args_type?)
|
49
|
+
end
|
50
|
+
|
51
|
+
def local_var_sends
|
52
|
+
return @local_var_sends if @local_var_sends
|
53
|
+
@local_var_sends = {}
|
54
|
+
|
55
|
+
local_var_send_nodes.map do |send_node|
|
56
|
+
object, method_sym = send_node.children[0..1]
|
57
|
+
local_var = object.children.first
|
58
|
+
@local_var_sends[local_var] ||= Set.new
|
59
|
+
@local_var_sends[local_var] << method_sym
|
60
|
+
end
|
61
|
+
|
62
|
+
@local_var_sends
|
63
|
+
end
|
64
|
+
|
65
|
+
def local_var_assigns
|
66
|
+
return @local_var_assigns if @local_var_assigns
|
67
|
+
@local_var_assigns = Set.new
|
68
|
+
@method_def.each_descendant do |node|
|
69
|
+
@local_var_assigns << node.children.first if node.lvasgn_type?
|
70
|
+
end
|
71
|
+
@local_var_assigns
|
72
|
+
end
|
73
|
+
|
74
|
+
def local_var_send_nodes
|
75
|
+
@method_def.each_descendant.select do |node|
|
76
|
+
node.children && node.children.first && node.send_type? &&
|
77
|
+
node.children.first.lvar_type?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'rubocop'
|
3
|
+
require 'parser/current'
|
4
|
+
Parser::Builders::Default.emit_lambda = true
|
5
|
+
|
6
|
+
module TypeTracer
|
7
|
+
class << self
|
8
|
+
def parse_file(file)
|
9
|
+
parse(File.read(file), file)
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse(source, filename = '(string)')
|
13
|
+
buffer = Parser::Source::Buffer.new(filename)
|
14
|
+
buffer.source = source
|
15
|
+
|
16
|
+
builder = RuboCop::Node::Builder.new
|
17
|
+
parser = Parser::CurrentRuby.new(builder)
|
18
|
+
parser.parse(buffer)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'type_tracer/type_sampler'
|
3
|
+
|
4
|
+
module TypeTracer
|
5
|
+
class TypeSamplerMiddleware
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
sample_decider = TypeTracer.config.rack_type_sample_decider
|
12
|
+
TypeSampler.start if sample_decider && sample_decider.call(env)
|
13
|
+
@app.call(env)
|
14
|
+
ensure
|
15
|
+
TypeSampler.stop
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# rake tasks for Rails 3+
|
3
|
+
module TypeTracer
|
4
|
+
class Railtie < ::Rails::Railtie
|
5
|
+
rake_tasks do
|
6
|
+
require 'type_tracer/rake/tasks'
|
7
|
+
end
|
8
|
+
|
9
|
+
initializer 'type_tracer.insert_middleware' do |app|
|
10
|
+
require 'type_tracer/rack/type_sampler_middleware.rb'
|
11
|
+
app.config.middleware.use 'TypeTracer::TypeSamplerMiddleware'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'type_tracer'
|
3
|
+
|
4
|
+
desc 'Checks for undefined instance methods'
|
5
|
+
namespace :type_tracer do
|
6
|
+
task check_method_calls: :environment do |_, _args|
|
7
|
+
root_dir = defined?(Rails) ? Rails.root : '.'
|
8
|
+
ruby_files = Dir.glob(root_dir.join('app/**/*.rb'))
|
9
|
+
ruby_files.each { |file| load(file) }
|
10
|
+
|
11
|
+
# Call the configured attribute methods definer to define
|
12
|
+
# methods on classes, e.g. initialize ActiveRecord models
|
13
|
+
TypeTracer.config.attribute_methods_definer.try(:call)
|
14
|
+
|
15
|
+
exit(1) unless undefined_method_messages(ruby_files).empty?
|
16
|
+
end
|
17
|
+
|
18
|
+
def undefined_method_messages(ruby_files)
|
19
|
+
ruby_files.flat_map do |file|
|
20
|
+
ast = TypeTracer.parse_file(file)
|
21
|
+
messages = TypeTracer::InstanceMethodChecker.new(ast).undefined_method_messages
|
22
|
+
messages.each(&method(:puts))
|
23
|
+
messages
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
task check_arg_sends: :environment do |_, _args|
|
28
|
+
root_dir = TypeTracer.config.type_check_root_path.to_s + '/'
|
29
|
+
files = Dir.glob(File.join(root_dir, '**/*.rb'))
|
30
|
+
TypeTracer.config.type_check_path_regex = %r{\A(app|lib)/}
|
31
|
+
type_check_files = files.select do |file|
|
32
|
+
project_path = file[root_dir.size..-1]
|
33
|
+
project_path =~ TypeTracer.config.type_check_path_regex
|
34
|
+
end
|
35
|
+
|
36
|
+
type_check_files.each { |file| load(file) }
|
37
|
+
|
38
|
+
runner = TypeTracer::ArgSendTypeCheck::Runner.new(type_check_files)
|
39
|
+
bad_arg_sends = runner.bad_arg_sends
|
40
|
+
bad_arg_sends.each { |message| puts message }
|
41
|
+
exit(1) if bad_arg_sends.present?
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module TypeTracer
|
3
|
+
class RealArgSendsChecker
|
4
|
+
def initialize(klass, symbol, args)
|
5
|
+
@klass = klass
|
6
|
+
@symbol = symbol
|
7
|
+
@args = args
|
8
|
+
end
|
9
|
+
|
10
|
+
def invalid_arg_messages
|
11
|
+
@args.each_with_index.flat_map(&method(:message_if_invalid_arg)).compact
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# arg_value is the real argument value that we would be calling this method
|
17
|
+
# with (based on the method call on the stub).
|
18
|
+
# index is the argument's position in the call
|
19
|
+
def message_if_invalid_arg(arg_value, index)
|
20
|
+
arg_sends = method_analyzer.arg_sends.values[index]
|
21
|
+
return unless arg_sends
|
22
|
+
|
23
|
+
# arg_sends is the list of definite method calls on that argument that
|
24
|
+
# the static analyzer detected. If there are branches in the method or
|
25
|
+
# assignments to that argument in the method, the analyzer is conservative
|
26
|
+
# and doesn't assume that the sends will necessarily be on this value.
|
27
|
+
arg_sends.each do |arg_send|
|
28
|
+
next if arg_value.respond_to?(arg_send)
|
29
|
+
|
30
|
+
# Give an invalid arg message if the arg doesn't respond to the call
|
31
|
+
# that the stubbed method would make on it.
|
32
|
+
return invalid_arg_message(arg_value, arg_send, index)
|
33
|
+
end
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def invalid_arg_message(arg_value, arg_send, index)
|
38
|
+
"Called stubbed method #{@klass}##{@symbol} with unrealistic "\
|
39
|
+
"'#{method_analyzer.arg_names[index]}' argument: #{arg_value.inspect}. "\
|
40
|
+
"It should respond to #{arg_send.inspect} but it does not."
|
41
|
+
end
|
42
|
+
|
43
|
+
def method_analyzer
|
44
|
+
@method_analyzer ||= MethodAnalyzer.new(method_def: method_ast)
|
45
|
+
end
|
46
|
+
|
47
|
+
def method_ast
|
48
|
+
@method_ast ||=
|
49
|
+
begin
|
50
|
+
file, _line = @klass.instance_method(@symbol).source_location
|
51
|
+
ast = TypeTracer.parse_file(file)
|
52
|
+
find_method_def(ast, @symbol)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def find_method_def(ast, method_symbol)
|
57
|
+
ast.each_descendant.find do |node|
|
58
|
+
node.def_type? && node.children.first == method_symbol
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'rspec/mocks'
|
3
|
+
require 'type_tracer'
|
4
|
+
require 'type_tracer/real_arg_sends_checker'
|
5
|
+
|
6
|
+
module RSpec
|
7
|
+
module Mocks
|
8
|
+
class VerifyingMethodDouble
|
9
|
+
alias orig_proxy_method_invoked proxy_method_invoked
|
10
|
+
|
11
|
+
def proxy_method_invoked(obj, *args, &block)
|
12
|
+
orig_proxy_method_invoked(obj, *args, &block)
|
13
|
+
return unless obj.is_a?(InstanceVerifyingDouble)
|
14
|
+
|
15
|
+
name = @method_reference.instance_variable_get('@method_name')
|
16
|
+
klass = @method_reference.instance_variable_get('@object_reference')
|
17
|
+
.instance_variable_get('@object')
|
18
|
+
|
19
|
+
checker = TypeTracer::RealArgSendsChecker.new(klass, name, args)
|
20
|
+
messages = checker.invalid_arg_messages
|
21
|
+
raise MockExpectationError.new, messages.join("\n") unless messages.empty?
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class SendsWatcher < BasicObject
|
3
|
+
def initialize(target, calls_list)
|
4
|
+
@target = target
|
5
|
+
@calls_list = calls_list
|
6
|
+
end
|
7
|
+
|
8
|
+
def method_missing(m, *args, &block)
|
9
|
+
@calls_list << m unless @calls_list.include?(m)
|
10
|
+
@target.send(m, *args, &block)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'net/http'
|
3
|
+
require 'json'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module TypeTracer
|
7
|
+
class TypeFetcher
|
8
|
+
class << self
|
9
|
+
def fetch_sampled_types
|
10
|
+
url = TypeTracer.config.sampled_types_url
|
11
|
+
puts "Using sampled types from: #{url}\n\n"
|
12
|
+
json = Net::HTTP.get(URI.parse(url))
|
13
|
+
save_types_locally(json)
|
14
|
+
JSON.parse(json).deep_symbolize_keys
|
15
|
+
end
|
16
|
+
|
17
|
+
def save_types_locally(types_json)
|
18
|
+
root_path = TypeTracer.config.type_check_root_path
|
19
|
+
folder = File.join(root_path, 'tmp', 'type_tracer')
|
20
|
+
FileUtils.mkdir_p(folder)
|
21
|
+
file = File.join(folder, 'sampled_types.json')
|
22
|
+
File.write(file, types_json)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'type_tracer/sends_watcher'
|
3
|
+
|
4
|
+
module TypeTracer
|
5
|
+
class TypeSampler
|
6
|
+
class << self
|
7
|
+
def start
|
8
|
+
@project_root ||= TypeTracer.config.type_check_root_path.to_s + '/'
|
9
|
+
@sample_path_regex ||= TypeTracer.config.type_check_path_regex
|
10
|
+
@ignored_classes ||= Set.new
|
11
|
+
@type_info_by_class ||= {}
|
12
|
+
@trace ||= TracePoint.new(:call, &method(:trace_method_call))
|
13
|
+
@trace.enable
|
14
|
+
end
|
15
|
+
|
16
|
+
def stop
|
17
|
+
return unless @trace && @trace.enabled?
|
18
|
+
@trace.disable
|
19
|
+
end
|
20
|
+
|
21
|
+
def sampled_type_info
|
22
|
+
{
|
23
|
+
git_commit: TypeTracer.config.git_commit,
|
24
|
+
type_info: @type_info_by_class
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def clear_sampled_type_info
|
29
|
+
@type_info_by_class = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def trace_method_call(tp)
|
35
|
+
klass = tp.defined_class
|
36
|
+
|
37
|
+
# Skip if the method call is in this class or an ignored class
|
38
|
+
return if klass == self.class || @ignored_classes.include?(klass)
|
39
|
+
|
40
|
+
unbound_method = unbound_method_or_nil(tp)
|
41
|
+
return unless unbound_method
|
42
|
+
|
43
|
+
if in_sample_path?(unbound_method.source_location[0])
|
44
|
+
add_sampled_type_info(tp, unbound_method)
|
45
|
+
else
|
46
|
+
@ignored_classes << klass
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def in_sample_path?(path)
|
51
|
+
return false unless path.start_with?(@project_root)
|
52
|
+
path[@project_root.size..-1] =~ @sample_path_regex
|
53
|
+
end
|
54
|
+
|
55
|
+
def unbound_method_or_nil(tp)
|
56
|
+
tp.defined_class.instance_method(tp.method_id)
|
57
|
+
rescue
|
58
|
+
# Return nil if the defined class fails to provide `instance_method`
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
def add_sampled_type_info(tp, unbound_method)
|
63
|
+
method_info = find_method_info(tp, unbound_method)
|
64
|
+
|
65
|
+
add_project_call_stack(method_info[:callers])
|
66
|
+
|
67
|
+
arg_names = method_info[:args].map { |a| a[1] }
|
68
|
+
add_args_type_info(tp, method_info[:arg_types], arg_names)
|
69
|
+
end
|
70
|
+
|
71
|
+
def find_method_info(tp, unbound_method)
|
72
|
+
klass = tp.defined_class
|
73
|
+
@type_info_by_class[klass] ||= {}
|
74
|
+
class_type_info = @type_info_by_class[klass]
|
75
|
+
class_type_info[tp.method_id] ||= default_method_info(unbound_method)
|
76
|
+
end
|
77
|
+
|
78
|
+
def default_method_info(unbound_method)
|
79
|
+
{ args: unbound_method.parameters, arg_types: {}, callers: [] }
|
80
|
+
end
|
81
|
+
|
82
|
+
def add_project_call_stack(call_stacks)
|
83
|
+
# Exclude non-project frames, and then also exclude the first project
|
84
|
+
# frame as that frame is for the method call we are type sampling.
|
85
|
+
stack = caller.select(&method(:in_sample_path?))[1..-1]
|
86
|
+
.map { |f| f[@project_root.size..-1] }
|
87
|
+
call_stacks << stack unless call_stacks.include?(stack)
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_args_type_info(tp, args_type_info, arg_names)
|
91
|
+
arg_local_vars = arg_names & tp.binding.local_variables
|
92
|
+
|
93
|
+
arg_local_vars.each do |arg|
|
94
|
+
args_type_info[arg] ||= {}
|
95
|
+
add_arg_type_info(tp, args_type_info[arg], arg)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def add_arg_type_info(tp, arg_type_info, arg)
|
100
|
+
value = tp.binding.local_variable_get(arg)
|
101
|
+
value_klass = value.class
|
102
|
+
|
103
|
+
arg_type_info[value_klass] ||= []
|
104
|
+
|
105
|
+
# We can only do do a delegate-based type watching on truthy
|
106
|
+
# values because it's not possible to turn a custom object into a
|
107
|
+
# falsely value in Ruby
|
108
|
+
return unless value && !value.is_a?(Fixnum)
|
109
|
+
watcher = SendsWatcher.new(value, arg_type_info[value_klass])
|
110
|
+
tp.binding.local_variable_set(arg, watcher)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/lib/type_tracer.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
Dir[File.join(File.dirname(__FILE__), 'type_tracer', '*.rb')].each do |file|
|
3
|
+
next if file =~ /version/
|
4
|
+
require File.join('type_tracer', File.basename(file, '.rb'))
|
5
|
+
end
|
6
|
+
require 'type_tracer/parser'
|
7
|
+
require 'type_tracer/arg_send_type_check/ast_checker'
|
8
|
+
require 'type_tracer/arg_send_type_check/method_checker'
|
9
|
+
require 'type_tracer/arg_send_type_check/runner'
|
10
|
+
require 'type_tracer/instance_method_checker'
|
11
|
+
require 'type_tracer/config'
|
12
|
+
require 'type_tracer/method_analyzer'
|
13
|
+
|
14
|
+
module TypeTracer
|
15
|
+
class << self
|
16
|
+
def config
|
17
|
+
@config ||= TypeTracer::Config.instance
|
18
|
+
yield @config if block_given?
|
19
|
+
@config
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'type_tracer/rails/railtie' if defined? Rails::Railtie
|
data/type_tracer.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'type_tracer/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'type_tracer'
|
9
|
+
spec.version = TypeTracer::VERSION
|
10
|
+
spec.authors = ['draffensperger']
|
11
|
+
spec.email = ['draff8660@gmail.com']
|
12
|
+
|
13
|
+
spec.summary = 'Proof of concept tool for Ruby static/dynamic analysis'
|
14
|
+
spec.description = 'Proof of concept tool for Ruby static/dynamic analysis'
|
15
|
+
spec.homepage = 'https://github.com/draffensperger/type_tracer'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0")
|
19
|
+
.reject { |f| f.match(%r{^(test|spec|features)/}) }
|
20
|
+
spec.bindir = 'exe'
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ['lib']
|
23
|
+
|
24
|
+
spec.add_runtime_dependency('parser', '>= 2.3.0.6', '< 3.0')
|
25
|
+
spec.add_runtime_dependency('activesupport', '~> 4.2.6')
|
26
|
+
spec.add_runtime_dependency('rubocop', '~> 0.39')
|
27
|
+
|
28
|
+
spec.add_development_dependency 'simplecov', '~> 0.11.2'
|
29
|
+
spec.add_development_dependency 'bundler', '~> 1.11'
|
30
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
31
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
32
|
+
spec.add_development_dependency 'pry-byebug'
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: type_tracer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- draffensperger
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-05-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: parser
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.3.0.6
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 2.3.0.6
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '3.0'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: activesupport
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 4.2.6
|
40
|
+
type: :runtime
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 4.2.6
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rubocop
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0.39'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0.39'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: simplecov
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 0.11.2
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 0.11.2
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: bundler
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '1.11'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '1.11'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rake
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '10.0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '10.0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: rspec
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '3.0'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '3.0'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: pry-byebug
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
description: Proof of concept tool for Ruby static/dynamic analysis
|
132
|
+
email:
|
133
|
+
- draff8660@gmail.com
|
134
|
+
executables: []
|
135
|
+
extensions: []
|
136
|
+
extra_rdoc_files: []
|
137
|
+
files:
|
138
|
+
- ".gitignore"
|
139
|
+
- ".rspec"
|
140
|
+
- ".rubocop.yml"
|
141
|
+
- ".travis.yml"
|
142
|
+
- Gemfile
|
143
|
+
- LICENSE.txt
|
144
|
+
- README.md
|
145
|
+
- Rakefile
|
146
|
+
- bin/console
|
147
|
+
- bin/rspec
|
148
|
+
- bin/setup
|
149
|
+
- lib/type_tracer.rb
|
150
|
+
- lib/type_tracer/arg_send_type_check/ast_checker.rb
|
151
|
+
- lib/type_tracer/arg_send_type_check/method_checker.rb
|
152
|
+
- lib/type_tracer/arg_send_type_check/runner.rb
|
153
|
+
- lib/type_tracer/config.rb
|
154
|
+
- lib/type_tracer/instance_method_checker.rb
|
155
|
+
- lib/type_tracer/method_analyzer.rb
|
156
|
+
- lib/type_tracer/parser.rb
|
157
|
+
- lib/type_tracer/rack/type_sampler_middleware.rb
|
158
|
+
- lib/type_tracer/rails/railtie.rb
|
159
|
+
- lib/type_tracer/rake/tasks.rb
|
160
|
+
- lib/type_tracer/real_arg_sends_checker.rb
|
161
|
+
- lib/type_tracer/rspec/instance_double_arg_checker.rb
|
162
|
+
- lib/type_tracer/sends_watcher.rb
|
163
|
+
- lib/type_tracer/type_fetcher.rb
|
164
|
+
- lib/type_tracer/type_sampler.rb
|
165
|
+
- lib/type_tracer/version.rb
|
166
|
+
- type_tracer.gemspec
|
167
|
+
homepage: https://github.com/draffensperger/type_tracer
|
168
|
+
licenses:
|
169
|
+
- MIT
|
170
|
+
metadata: {}
|
171
|
+
post_install_message:
|
172
|
+
rdoc_options: []
|
173
|
+
require_paths:
|
174
|
+
- lib
|
175
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
181
|
+
requirements:
|
182
|
+
- - ">="
|
183
|
+
- !ruby/object:Gem::Version
|
184
|
+
version: '0'
|
185
|
+
requirements: []
|
186
|
+
rubyforge_project:
|
187
|
+
rubygems_version: 2.6.4
|
188
|
+
signing_key:
|
189
|
+
specification_version: 4
|
190
|
+
summary: Proof of concept tool for Ruby static/dynamic analysis
|
191
|
+
test_files: []
|