gelauto 1.0.0

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: edff254e0369ac872f616b28e6885f92efb5127f0ddd159d477d545015a19f48
4
+ data.tar.gz: 7645cfbff7c9c2806b8c160ce27aa2f8119f6c56f62362655481e44a26f720d4
5
+ SHA512:
6
+ metadata.gz: 363f769d6dec4cec5a5390bdd099418b53785af49a61cc85a410bc125a66e5ad047fa403ca8220fa7e56ec060c9216f3ee63eacd83dfc8801a90591afee41b43
7
+ data.tar.gz: abee9406ff027155948e07b0be22cdfbcbd2b9744f492508cd90380ede41f792df120719a5ef4f143c783b52b9ea59269d412e7b4fa629ca083c7e1d08cd1d1c
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem 'pry-byebug'
7
+ gem 'rspec'
8
+ gem 'rake'
9
+ end
data/README.md ADDED
@@ -0,0 +1,139 @@
1
+ ## gelauto [![Build Status](https://secure.travis-ci.org/camertron/gelauto.png?branch=master)](http://travis-ci.org/camertron/gelauto)
2
+
3
+ Automatically annotate your code with Sorbet type definitions.
4
+
5
+ ## What is This Thing?
6
+
7
+ The wonderful folks at Stripe recently released a static type checker for Ruby called [Sorbet](https://github.com/sorbet/sorbet). It works by examining type signatures placed at the beginning of each method. For example:
8
+
9
+ ```ruby
10
+ # typed: true
11
+
12
+ class Car
13
+ extend T::Sig
14
+
15
+ sig { params(num_wheels: Integer) }
16
+ def initialize(num_wheels)
17
+ @num_wheels = num_wheels
18
+ end
19
+
20
+ sig { params(speed: Float).returns(T::Boolean) }
21
+ def drive(speed)
22
+ true
23
+ end
24
+ end
25
+ ```
26
+
27
+ Adding these definitions means you get cool stuff like auto-complete and type checking in your editor. Pretty freaking rad.
28
+
29
+ ### Ok, so what is Gelauto?
30
+
31
+ Gelauto is an _auto_-matic way (get it?! lol) of adding Sorbet type signatures to your methods. It works by running your code (for example, your test suite). As your code runs, Gelauto keeps track of the actual types of objects that were passed to your methods as well as the types of objects they return. After gathering the info, Gelauto then (optionally) inserts type signatures into your Ruby files.
32
+
33
+ ## Installation
34
+
35
+ `gem install gelauto`
36
+
37
+ ## Usage
38
+
39
+ You can run Gelauto either via the command line or by adding it to your bundle.
40
+
41
+ ### Command Line Usage
42
+
43
+ First, install the gem by running `gem install gelauto`. That will make the `gelauto` executable available on your system.
44
+
45
+ Gelauto's only subcommand is `run`, which accepts a list of Ruby files to scan for methods and a command to run that will exercise your code. In this example, we're going to be running an [RSpec](https://github.com/rspec/rspec) test suite.
46
+
47
+ Like most RSpec test suites, let's assume ours is stored in the `spec/` directory (that's the RSpec default too). Let's furthermore assume our code is stored in the `lib` directory. To run the test suite in `spec/` and add type definitions to our files in `lib/`, we might run the following command:
48
+
49
+ ```bash
50
+ gelauto run --annotate $(find . -name '*.rb') -- bundle exec rspec spec/
51
+ ```
52
+
53
+ If you choose to run Gelauto without the `--annotate` flag, Gelauto will print results to standard output in [RBI format](https://sorbet.org/docs/rbi) and not modify any of your Ruby files.
54
+
55
+ ### Gelauto in your Bundle
56
+
57
+ If you would rather run Gelauto as part of your bundle, add it to your Gemfile like so:
58
+
59
+ ```ruby
60
+ gem 'gelauto'
61
+ ```
62
+
63
+ Gelauto can be invoked from within your code in one of several ways.
64
+
65
+ #### Gelauto.discover
66
+
67
+ Wrap code you'd like to run with Gelauto in `Gelauto.discover`:
68
+
69
+ ```ruby
70
+ require 'gelauto'
71
+
72
+ Gelauto.paths << 'path/to/file/i/want/to/annotate.rb'
73
+
74
+ Gelauto.discover do
75
+ call_some_method(with, some, params)
76
+ end
77
+
78
+ # loop over files and annotate them
79
+ Gelauto.each_absolute_path do |path|
80
+ Gelauto.annotate_file(path)
81
+ end
82
+
83
+ # you can also grab a reference to the method cache Gelauto
84
+ # has populated with all the type information it's been able
85
+ # to gather:
86
+ Gelauto.method_index
87
+ ```
88
+
89
+ #### Setup and Teardown
90
+
91
+ `Gelauto.discover` is just syntactic sugar around two methods that start and stop Gelauto's method tracing functionality:
92
+
93
+ ```ruby
94
+ Gelauto.setup
95
+
96
+ begin
97
+ call_some_method(with, some, params)
98
+ ensure
99
+ Gelauto.teardown
100
+ end
101
+ ```
102
+
103
+ #### RSpec Helper
104
+
105
+ Gelauto comes with a handy RSpec helper that can do all this for you. Simply add
106
+
107
+ ```ruby
108
+ require 'gelauto/rspec'
109
+ ```
110
+
111
+ to your spec_helper.rb, Rakefile, or wherever RSpec is configured.
112
+
113
+ ## How does it Work?
114
+
115
+ Gelauto makes use of Ruby's [TracePoint API](https://ruby-doc.org/core-2.6/TracePoint.html). TracePoint effectively allows Gelauto to receive a notification whenever a Ruby method is called and whenever a method returns. That info combined with method location information gathered from parsing your Ruby files ahead of time allows Gelauto to know a) where methods are located, 2) what arguments they take, 3) the types of those arguments, and 4) the type of the return value.
116
+
117
+ "Doesn't that potentially make my code run slower?" is a question you might ask. Yes. Gelauto adds overhead to literally every Ruby method call, so your code will probably run quite a bit slower. For that reason you probably won't want to enable Gelauto in, for example, a production web application.
118
+
119
+ ## Known Limitations
120
+
121
+ * Half-baked support for singleton (i.e. static) methods.
122
+ * Gelauto does not annotate Ruby files with `# typed: true` comments or `extend T::Sig`.
123
+ * Gelauto ignores existing type signatures and will simply add another one right above the method.
124
+
125
+ ## Running Tests
126
+
127
+ `bundle exec rspec` should do the trick :)
128
+
129
+ ## Contributing
130
+
131
+ Please fork this repo and submit a pull request.
132
+
133
+ ## License
134
+
135
+ Licensed under the MIT license. See LICENSE for details.
136
+
137
+ ## Authors
138
+
139
+ * Cameron C. Dutro: http://github.com/camertron
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+ require 'rubygems/package_task'
4
+
5
+ require 'gelauto'
6
+
7
+ Bundler::GemHelper.install_tasks
8
+
9
+ task default: :spec
10
+
11
+ desc 'Run specs'
12
+ RSpec::Core::RakeTask.new do |t|
13
+ t.pattern = './spec/**/*_spec.rb'
14
+ end
data/bin/gelauto ADDED
@@ -0,0 +1,54 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'gelauto'
4
+ require 'gli'
5
+
6
+ module Gelauto
7
+ module CLI
8
+ extend GLI::App
9
+
10
+ program_desc 'Automatically annotate methods with Sorbet type signatures.'
11
+
12
+ version Gelauto::VERSION
13
+
14
+ subcommand_option_handling :normal
15
+ default_command :run
16
+
17
+ desc 'Run the given command with Gelauto and optionally annotate files.'
18
+ command :run do |c|
19
+ c.desc 'Write discovered type signatures into Ruby files.'
20
+ c.default_value false
21
+ c.switch [:a, :annotate]
22
+
23
+ c.action do |global_options, options, args|
24
+ paths, _, cmd = args.chunk_while { |arg1, arg2| arg1 != '--' && arg2 != '--' }.to_a
25
+ Gelauto.paths += paths
26
+
27
+ exe = Gelauto::CLIUtils.which(cmd[0]) || cmd[0]
28
+ cmd.shift
29
+
30
+ old_argv = ARGV.dup
31
+
32
+ begin
33
+ Gelauto.setup
34
+ ARGV.replace(cmd)
35
+ load exe
36
+ ensure
37
+ Gelauto.teardown
38
+ ARGV.replace(old_argv)
39
+
40
+ if options[:annotate]
41
+ Gelauto.each_absolute_path do |path|
42
+ Gelauto.annotate_file(path)
43
+ Gelauto::Logger.info("Annotated #{path}")
44
+ end
45
+ else
46
+ puts Gelauto::Rbi.new(Gelauto.method_index).to_s
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ exit Gelauto::CLI.run(ARGV)
data/gelauto.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), 'lib')
2
+ require 'gelauto/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'gelauto'
6
+ s.version = ::Gelauto::VERSION
7
+ s.authors = ['Cameron Dutro']
8
+ s.email = ['camertron@gmail.com']
9
+ s.homepage = 'http://github.com/camertron/gelauto'
10
+
11
+ s.description = s.summary = 'Automatically annotate your code with Sorbet type definitions.'
12
+ s.platform = Gem::Platform::RUBY
13
+
14
+ s.add_dependency 'parser', '~> 2.6'
15
+ s.add_dependency 'gli', '~> 2.0'
16
+
17
+ s.executables << 'gelauto'
18
+
19
+ s.require_path = 'lib'
20
+ s.files = Dir['{lib,spec}/**/*', 'Gemfile', 'README.md', 'Rakefile', 'gelauto.gemspec']
21
+ end
@@ -0,0 +1,32 @@
1
+ module Gelauto
2
+ class ArgList
3
+ include Enumerable
4
+
5
+ attr_reader :args
6
+
7
+ def initialize(args = [])
8
+ @args = args
9
+ end
10
+
11
+ def <<(arg)
12
+ args << arg
13
+ end
14
+
15
+ def [](idx)
16
+ args[idx]
17
+ end
18
+
19
+ def empty?
20
+ args.empty?
21
+ end
22
+
23
+ def each(&block)
24
+ args.each(&block)
25
+ end
26
+
27
+ def to_sig
28
+ return '' if args.empty?
29
+ args.map(&:to_sig).join(', ')
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,25 @@
1
+ module Gelauto
2
+ class ArrayType < GenericType
3
+ def self.introspect(obj)
4
+ new.tap do |var|
5
+ obj.each { |elem| var[:elem] << Gelauto.introspect(elem) }
6
+ end
7
+ end
8
+
9
+ def initialize
10
+ super(::Array, [:elem])
11
+ end
12
+
13
+ def to_sig
14
+ if self[:elem].empty?
15
+ 'T::Array'
16
+ else
17
+ "T::Array[#{self[:elem].to_sig}]"
18
+ end
19
+ end
20
+
21
+ def merge!(other)
22
+ self[:elem].merge!(other[:elem])
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ module Gelauto
2
+ class BooleanType < Type
3
+ def to_sig
4
+ 'T::Boolean'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ module Gelauto
2
+ module CLIUtils
3
+ EXTS = (ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']).freeze
4
+
5
+ def which(cmd)
6
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
7
+ EXTS.each do |ext|
8
+ exe = File.join(path, "#{cmd}#{ext}")
9
+ return exe if File.executable?(exe) && !File.directory?(exe)
10
+ end
11
+ end
12
+
13
+ nil
14
+ end
15
+
16
+ extend self
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ require 'set'
2
+
3
+ module Gelauto
4
+ class GenericType
5
+ attr_reader :ruby_type, :generic_args
6
+
7
+ def initialize(ruby_type, generic_arg_names = [])
8
+ @ruby_type = ruby_type
9
+ @generic_args = {}
10
+
11
+ generic_arg_names.each_with_object({}) do |generic_arg_name, ret|
12
+ generic_args[generic_arg_name] = TypeSet.new
13
+ end
14
+ end
15
+
16
+ def [](generic_arg_name)
17
+ generic_args[generic_arg_name]
18
+ end
19
+
20
+ def to_sig
21
+ raise NotImplementedError, "please define #{__method__} in derived classes"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ module Gelauto
2
+ class HashType < GenericType
3
+ def self.introspect(obj)
4
+ new.tap do |var|
5
+ obj.each_pair do |key, value|
6
+ var[:key] << Gelauto.introspect(key)
7
+ var[:value] << Gelauto.introspect(value)
8
+ end
9
+ end
10
+ end
11
+
12
+ def initialize
13
+ super(::Hash, [:key, :value])
14
+ end
15
+
16
+ def to_sig
17
+ if self[:key].empty? && self[:value].empty?
18
+ 'T::Hash'
19
+ else
20
+ "T::Hash[#{self[:key].to_sig}, #{self[:value].to_sig}]"
21
+ end
22
+ end
23
+
24
+ def merge!(other)
25
+ self[:key].merge!(other[:key])
26
+ self[:value].merge!(other[:value])
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,31 @@
1
+ module Gelauto
2
+ module Logger
3
+ class << self
4
+ def debug(str)
5
+ Gelauto.logger.debug(fmt(str))
6
+ end
7
+
8
+ def info(str)
9
+ Gelauto.logger.info(fmt(str))
10
+ end
11
+
12
+ def warn(str)
13
+ Gelauto.logger.warn(fmt(str))
14
+ end
15
+
16
+ def error(str)
17
+ Gelauto.logger.error(fmt(str))
18
+ end
19
+
20
+ def fatal(str)
21
+ Gelauto.logger.fatal(fmt(str))
22
+ end
23
+
24
+ private
25
+
26
+ def fmt(str)
27
+ "[Gelauto] #{str}"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,34 @@
1
+ require 'set'
2
+
3
+ module Gelauto
4
+ class MethodDef
5
+ attr_reader :name, :args, :nesting, :return_types
6
+
7
+ def initialize(name, args, nesting, return_types = TypeSet.new)
8
+ @name = name
9
+ @args = ArgList.new(args.map { |arg| Var.new(arg) })
10
+ @nesting = nesting
11
+ @return_types = return_types
12
+ end
13
+
14
+ def to_sig
15
+ components = []
16
+
17
+ unless args.empty?
18
+ components << "params(#{args.to_sig})"
19
+ end
20
+
21
+ if name == :initialize
22
+ components << 'void'
23
+ else
24
+ components << "returns(#{return_types.to_sig})"
25
+ end
26
+
27
+ "sig { #{components.join('.')} }"
28
+ end
29
+
30
+ def to_rbi
31
+ "#{to_sig}\ndef #{name}; end"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,97 @@
1
+ module Gelauto
2
+ class MethodIndex
3
+ attr_reader :index
4
+
5
+ def initialize
6
+ @index = Hash.new { |h, k| h[k] = {} }
7
+ end
8
+
9
+ def index_methods_in(path, ast, nesting = [])
10
+ return unless ast
11
+
12
+ case ast.type
13
+ when :def, :defs
14
+ name = nil
15
+ args = nil
16
+
17
+ if ast.type == :def
18
+ name = ast.children[0]
19
+ args = ast.children[1].children.map { |c| c.children.first }
20
+ else
21
+ name = ast.children[1]
22
+ args = ast.children[2].children.map { |c| c.children.first }
23
+ end
24
+
25
+ md = MethodDef.new(name, args, nesting)
26
+ index[path][ast.location.name.line] = md
27
+
28
+ # point to start of method
29
+ index[path][ast.location.end.line] = ast.location.name.line
30
+
31
+ when :class, :module
32
+ const_name = ast.children.first.children.last
33
+ return visit_children(path, ast, nesting + [Namespace.new(const_name, ast.type)])
34
+ end
35
+
36
+ visit_children(path, ast, nesting)
37
+ end
38
+
39
+ def find(path, lineno)
40
+ if md = index[path][lineno]
41
+ return md if md.is_a?(MethodDef)
42
+
43
+ # md is actually an index pointing to another line
44
+ index[path][md]
45
+ end
46
+ end
47
+
48
+ def each
49
+ return to_enum(__method__) unless block_given?
50
+
51
+ index.each_pair do |path, line_index|
52
+ line_index.each_pair do |lineno, md|
53
+ yield(path, lineno, md) if md.is_a?(MethodDef)
54
+ end
55
+ end
56
+ end
57
+
58
+ def each_method_in(path)
59
+ return to_enum(__method__, path) unless block_given?
60
+
61
+ index[path].each_pair do |lineno, md|
62
+ yield lineno, md if md.is_a?(MethodDef)
63
+ end
64
+ end
65
+
66
+ def annotate(path, code)
67
+ lines = code.split(/\r?\n/)
68
+ mds = index[path]
69
+
70
+ [].tap do |annotated|
71
+ lines.each_with_index do |line, idx|
72
+ lineno = idx + 1
73
+ md = mds[lineno]
74
+
75
+ if md.is_a?(MethodDef)
76
+ indent = line[0...line.index(/[^\s]/)]
77
+ annotated << "#{indent}#{md.to_sig}"
78
+ end
79
+
80
+ annotated << line
81
+ end
82
+ end.join("\n")
83
+ end
84
+
85
+ def reset
86
+ index.clear
87
+ end
88
+
89
+ private
90
+
91
+ def visit_children(path, ast, nesting)
92
+ ast.children.each do |child|
93
+ index_methods_in(path, child, nesting) if child.is_a?(Parser::AST::Node)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,10 @@
1
+ module Gelauto
2
+ class Namespace
3
+ attr_reader :name, :type
4
+
5
+ def initialize(name, type)
6
+ @name = name
7
+ @type = type
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,47 @@
1
+ module Gelauto
2
+ class Rbi
3
+ attr_reader :method_index, :paths
4
+
5
+ def initialize(method_index, paths = Gelauto.paths)
6
+ @method_index = method_index
7
+ @paths = paths
8
+ end
9
+
10
+ def to_s
11
+ StringIO.new.tap { |io| compose(method_groups, io) }.string
12
+ end
13
+
14
+ private
15
+
16
+ def method_groups
17
+ @method_groups ||= { methods: [], children: {} }.tap do |groups|
18
+ Utils.each_absolute_path(paths) do |path|
19
+ method_index.each_method_in(path) do |_lineno, md|
20
+ cur_group = md.nesting.inject(groups) do |group, namespace|
21
+ group[:children][[namespace.name, namespace.type]] ||= { methods: [], children: {} }
22
+ end
23
+
24
+ cur_group[:methods] << md
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ def compose(h, io, indent_level = 0)
31
+ io.write(indent(h[:methods].map { |md| md.to_rbi }.join("\n\n"), indent_level))
32
+
33
+ h[:children].each_with_index do |((namespace, type), next_level), idx|
34
+ io.write("\n\n") if idx > 0
35
+ io.write(indent("#{type} #{namespace}", indent_level))
36
+ io.write("\n")
37
+ compose(next_level, io, indent_level + 1)
38
+ io.write("\n")
39
+ io.write(indent('end', indent_level))
40
+ end
41
+ end
42
+
43
+ def indent(str, indent_level)
44
+ str.split("\n").map { |s| "#{' ' * indent_level}#{s}" }.join("\n")
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,13 @@
1
+ require 'gelauto'
2
+
3
+ RSpec.configure do |config|
4
+ config.before(:suite) { Gelauto.setup }
5
+ config.after(:suite) do
6
+ Gelauto.teardown
7
+
8
+ Gelauto.each_absolute_path do |path|
9
+ Gelauto.annotate_file(path)
10
+ Gelauto::Logger.info("Annotated #{path}")
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ module Gelauto
2
+ class Type
3
+ def self.introspect(obj)
4
+ new(obj.class)
5
+ end
6
+
7
+ attr_reader :ruby_type
8
+
9
+ def initialize(ruby_type)
10
+ @ruby_type = ruby_type
11
+ end
12
+
13
+ def to_sig
14
+ # i.e. class name
15
+ ruby_type.name
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,61 @@
1
+ require 'set'
2
+
3
+ module Gelauto
4
+ class TypeSet
5
+ include Enumerable
6
+
7
+ attr_reader :set
8
+
9
+ def initialize
10
+ @set = {}
11
+ end
12
+
13
+ def size
14
+ set.size
15
+ end
16
+
17
+ def empty?
18
+ set.empty?
19
+ end
20
+
21
+ def <<(type)
22
+ set[type.ruby_type] = type
23
+ end
24
+
25
+ def merge!(other)
26
+ other.set.each do |other_ruby_type, other_type|
27
+ if set[other_ruby_type]
28
+ set[other_ruby_type].merge!(other_type)
29
+ else
30
+ set[other_ruby_type] = other_type
31
+ end
32
+ end
33
+ end
34
+
35
+ def each(&block)
36
+ set.each_value(&block)
37
+ end
38
+
39
+ def to_sig
40
+ nilable = false
41
+
42
+ sigs = set.each_with_object([]) do |(rt, t), ret|
43
+ if rt == ::NilClass
44
+ nilable = true
45
+ else
46
+ ret << t.to_sig
47
+ end
48
+ end
49
+
50
+ sigs.uniq!
51
+
52
+ if sigs.size == 0
53
+ 'T.untyped'
54
+ elsif sigs.size == 1
55
+ sigs.first
56
+ else
57
+ "T.#{nilable ? 'nilable' : 'any'}(#{sigs.join(', ')})"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,13 @@
1
+ module Gelauto
2
+ module Utils
3
+ def each_absolute_path(paths)
4
+ return to_enum(__method__, paths) unless block_given?
5
+
6
+ paths.each do |path|
7
+ yield File.expand_path(path)
8
+ end
9
+ end
10
+
11
+ extend self
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ require 'set'
2
+
3
+ module Gelauto
4
+ class Var
5
+ attr_reader :name
6
+ attr_accessor :types
7
+
8
+ def initialize(name, types = TypeSet.new)
9
+ @name = name
10
+ @types = types
11
+ end
12
+
13
+ def to_sig
14
+ "#{name}: #{types.to_sig}"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Gelauto
2
+ VERSION = '1.0.0'
3
+ end
data/lib/gelauto.rb ADDED
@@ -0,0 +1,124 @@
1
+ require 'logger'
2
+ require 'parser/current'
3
+
4
+ module Gelauto
5
+ autoload :ArgList, 'gelauto/arg_list'
6
+ autoload :ArrayType, 'gelauto/array_type'
7
+ autoload :BooleanType, 'gelauto/boolean_type'
8
+ autoload :CLIUtils, 'gelauto/cli_utils'
9
+ autoload :GenericType, 'gelauto/generic_type'
10
+ autoload :HashType, 'gelauto/hash_type'
11
+ autoload :Logger, 'gelauto/logger'
12
+ autoload :MethodDef, 'gelauto/method_def'
13
+ autoload :MethodIndex, 'gelauto/method_index'
14
+ autoload :Namespace, 'gelauto/namespace'
15
+ autoload :Rbi, 'gelauto/rbi'
16
+ autoload :Type, 'gelauto/type'
17
+ autoload :TypeSet, 'gelauto/type_set'
18
+ autoload :Utils, 'gelauto/utils'
19
+ autoload :Var, 'gelauto/var'
20
+
21
+ class << self
22
+ attr_accessor :paths
23
+ attr_writer :logger
24
+
25
+ def setup
26
+ enable_traces
27
+ index_methods
28
+ end
29
+
30
+ def teardown
31
+ disable_traces
32
+ end
33
+
34
+ def discover
35
+ setup
36
+ yield
37
+ ensure
38
+ teardown
39
+ end
40
+
41
+ def method_index
42
+ @method_index ||= MethodIndex.new
43
+ end
44
+
45
+ def paths
46
+ @paths ||= []
47
+ end
48
+
49
+ def each_absolute_path(&block)
50
+ Utils.each_absolute_path(paths, &block)
51
+ end
52
+
53
+ def register_type(type, handler)
54
+ types[type] = handler
55
+ end
56
+
57
+ def types
58
+ @types ||= Hash.new(Gelauto::Type)
59
+ end
60
+
61
+ def introspect(obj)
62
+ Gelauto.types[obj.class].introspect(obj)
63
+ end
64
+
65
+ def annotate_file(path)
66
+ annotated_code = Gelauto.method_index.annotate(path, File.read(path))
67
+ File.write(path, annotated_code)
68
+ end
69
+
70
+ def logger
71
+ @logger ||= ::Logger.new(STDOUT)
72
+ end
73
+
74
+ private
75
+
76
+ def enable_traces
77
+ call_trace.enable
78
+ return_trace.enable
79
+ end
80
+
81
+ def disable_traces
82
+ call_trace.disable
83
+ return_trace.disable
84
+ end
85
+
86
+ def index_methods
87
+ each_absolute_path.with_index do |path, idx|
88
+ begin
89
+ method_index.index_methods_in(
90
+ path, Parser::CurrentRuby.parse(File.read(path))
91
+ )
92
+
93
+ Gelauto::Logger.info("Indexed #{idx + 1}/#{paths.size} paths")
94
+ rescue Parser::SyntaxError => e
95
+ Gelauto::Logger.error("Syntax error in #{path}, skipping")
96
+ end
97
+ end
98
+ end
99
+
100
+ def call_trace
101
+ @call_trace ||= TracePoint.new(:call) do |tp|
102
+ if md = method_index.find(tp.path, tp.lineno)
103
+ md.args.each do |arg|
104
+ var = tp.binding.local_variable_get(arg.name)
105
+ arg.types << Gelauto.introspect(var)
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ def return_trace
112
+ @return_trace ||= TracePoint.new(:return) do |tp|
113
+ if md = method_index.find(tp.path, tp.lineno)
114
+ md.return_types << Gelauto.introspect(tp.return_value)
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ Gelauto.register_type(::Hash, Gelauto::HashType)
122
+ Gelauto.register_type(::Array, Gelauto::ArrayType)
123
+ Gelauto.register_type(::TrueClass, Gelauto::BooleanType)
124
+ Gelauto.register_type(::FalseClass, Gelauto::BooleanType)
@@ -0,0 +1,104 @@
1
+ require 'spec_helper'
2
+
3
+ describe Gelauto do
4
+ before { index.reset }
5
+
6
+ let(:index) { Gelauto.method_index }
7
+
8
+ context 'with simple types' do
9
+ it 'identifies method signatures correctly' do
10
+ img = nil
11
+
12
+ Gelauto.discover do
13
+ img = GelautoSpecs::Image.new('foo.jpg', 800, 400)
14
+ expect(img.aspect_ratio).to eq(2.0)
15
+ end
16
+
17
+ init = get_indexed_method(img, :initialize)
18
+ expect(init).to accept(path: String, width: Integer, height: Integer)
19
+ expect(init).to hand_back_void
20
+
21
+ aspect_ratio = get_indexed_method(img, :aspect_ratio)
22
+ expect(aspect_ratio).to hand_back(Float)
23
+ end
24
+ end
25
+
26
+ context 'with generic types' do
27
+ before do
28
+ Gelauto.discover do
29
+ @client = GelautoSpecs::Client.new(url: 'http://foo.com', username: 'bar')
30
+ @response = @client.request('body', param1: 'abc', param2: 'def')
31
+ expect(@response.to_a).to eq([200, 'it worked!'])
32
+ end
33
+ end
34
+
35
+ it 'identifies signature for Client#initialize' do
36
+ init = get_indexed_method(@client, :initialize)
37
+ expect(init).to accept(options: { Hash => { key: Symbol, value: String } })
38
+ expect(init).to hand_back_void
39
+ end
40
+
41
+ it 'identifies signature for Client#request' do
42
+ request = get_indexed_method(@client, :request)
43
+ expect(request).to accept(body: String, headers: { Hash => { key: Symbol, value: String } })
44
+ expect(request).to hand_back(GelautoSpecs::Response)
45
+ end
46
+
47
+ it 'identifies signature for Response#initialize' do
48
+ init = get_indexed_method(@response, :initialize)
49
+ expect(init).to accept(status: Integer, body: String)
50
+ expect(init).to hand_back_void
51
+ end
52
+
53
+ it 'identifies signature for Response#to_a' do
54
+ to_a = get_indexed_method(@response, :to_a)
55
+ expect(to_a).to hand_back(Array => { elem: [Integer, String] })
56
+ end
57
+ end
58
+
59
+ context 'with nested generic types' do
60
+ before do
61
+ Gelauto.discover do
62
+ GelautoSpecs::System.configure(YAML.load_file('spec/support/config.yml'))
63
+ end
64
+ end
65
+
66
+ it 'identifies signatures for System.configure' do
67
+ configure = get_indexed_method(GelautoSpecs::System, :configure)
68
+ expect(configure).to accept(
69
+ config: {
70
+ Hash => {
71
+ key: String,
72
+ value: [
73
+ {
74
+ Array => {
75
+ elem: [
76
+ String,
77
+ {
78
+ Hash => {
79
+ key: String,
80
+ value: {
81
+ Hash => {
82
+ key: String,
83
+ value: [
84
+ String,
85
+ {
86
+ Array => {
87
+ elem: String
88
+ }
89
+ }
90
+ ]
91
+ }
92
+ }
93
+ }
94
+ }
95
+ ]
96
+ }
97
+ }
98
+ ]
99
+ }
100
+ }
101
+ )
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,28 @@
1
+ $:.push(File.dirname(__FILE__))
2
+
3
+ require 'rspec'
4
+ require 'gelauto'
5
+ require 'pry-byebug'
6
+
7
+ Dir.chdir('spec') do
8
+ Dir.glob('support/*.rb').each do |f|
9
+ require f
10
+ end
11
+ end
12
+
13
+ RSpec.configure do |config|
14
+ config.include(GelautoSpecs::AcceptMatcher)
15
+ config.include(GelautoSpecs::HandBackMatcher)
16
+
17
+ config.include(
18
+ Module.new do
19
+ def get_indexed_method(obj, method_name)
20
+ path, lineno = obj.method(method_name).source_location
21
+ Gelauto.method_index.find(path, lineno)
22
+ end
23
+ end
24
+ )
25
+ end
26
+
27
+ # quiet logs for test runs
28
+ Gelauto.logger.level = :fatal
@@ -0,0 +1,29 @@
1
+ module GelautoSpecs
2
+ class Response
3
+ attr_reader :status, :body
4
+
5
+ def initialize(status, body)
6
+ @status = status
7
+ @body = body
8
+ end
9
+
10
+ def to_a
11
+ [status, body]
12
+ end
13
+ end
14
+
15
+ class Client
16
+ attr_reader :url, :username
17
+
18
+ def initialize(options = {})
19
+ @url = options[:url]
20
+ @username = options[:username]
21
+ end
22
+
23
+ def request(body, headers = {})
24
+ Response.new(200, 'it worked!')
25
+ end
26
+ end
27
+ end
28
+
29
+ Gelauto.paths << __FILE__
@@ -0,0 +1,8 @@
1
+ param1:
2
+ - item1
3
+ - item2
4
+ - item3:
5
+ nested1: foo
6
+ nested2:
7
+ - bar
8
+ - baz
@@ -0,0 +1,17 @@
1
+ module GelautoSpecs
2
+ class Image
3
+ attr_reader :path, :width, :height
4
+
5
+ def initialize(path, width, height)
6
+ @path = path
7
+ @width = width
8
+ @height = height
9
+ end
10
+
11
+ def aspect_ratio
12
+ width.to_f / height
13
+ end
14
+ end
15
+ end
16
+
17
+ Gelauto.paths << __FILE__
@@ -0,0 +1,103 @@
1
+ module GelautoSpecs
2
+ def self.arg_hash_to_arglist(params)
3
+ Gelauto::ArgList.new.tap do |arg_list|
4
+ params.each_pair do |name, type_info|
5
+ arg_list << case type_info
6
+ when Hash
7
+ Gelauto::Var.new(name).tap do |v|
8
+ v.types.merge!(type_hash_to_typeset(type_info))
9
+ end
10
+ else
11
+ Gelauto::Var.new(name).tap do |v|
12
+ Array(type_info).each { |t| v.types << Gelauto::Type.new(t) }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ def self.type_hash_to_typeset(type_info)
20
+ Gelauto::TypeSet.new.tap do |typeset|
21
+ type_info.each_pair do |type, generics|
22
+ type = Gelauto.types[type].new
23
+
24
+ generics.each_pair do |generic_name, generic_type|
25
+ type.generic_args[generic_name].merge!(
26
+ case generic_type
27
+ when Hash
28
+ type_hash_to_typeset(generic_type)
29
+ else
30
+ type_array_to_typeset(Array(generic_type))
31
+ end
32
+ )
33
+ end
34
+
35
+ typeset << type
36
+ end
37
+ end
38
+ end
39
+
40
+ def self.type_array_to_typeset(params)
41
+ Gelauto::TypeSet.new.tap do |typeset|
42
+ params.each do |type_info|
43
+ case type_info
44
+ when Hash
45
+ typeset.merge!(type_hash_to_typeset(type_info))
46
+ when Array
47
+ typeset.merge!(type_array_to_typeset(type_info))
48
+ else
49
+ typeset << Gelauto::Type.new(type_info)
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ module AcceptMatcher
56
+ extend RSpec::Matchers::DSL
57
+
58
+ matcher :accept do |expected_params = {}|
59
+ match do |actual_md|
60
+ @expected_arg_list = GelautoSpecs.arg_hash_to_arglist(expected_params)
61
+ @expected_arg_list.to_sig == actual_md.args.to_sig
62
+ end
63
+
64
+ failure_message do |actual_md|
65
+ <<~END
66
+ Expected: #{@expected_arg_list.to_sig}
67
+ Got: #{actual_md.args.to_sig}
68
+ END
69
+ end
70
+ end
71
+ end
72
+
73
+ module HandBackMatcher
74
+ extend RSpec::Matchers::DSL
75
+
76
+ matcher :hand_back do |*expected_types|
77
+ match do |actual_md|
78
+ @expected_typeset = GelautoSpecs.type_array_to_typeset(expected_types)
79
+ @expected_typeset.to_sig == actual_md.return_types.to_sig
80
+ end
81
+
82
+ failure_message do |actual_md|
83
+ <<~END
84
+ Expected: #{@expected_typeset.to_sig}
85
+ Got: #{actual_md.return_types.to_sig}
86
+ END
87
+ end
88
+ end
89
+
90
+ matcher :hand_back_void do
91
+ match do |actual_md|
92
+ actual_md.to_sig.end_with?('.void }')
93
+ end
94
+
95
+ failure_message do |actual_md|
96
+ <<~END
97
+ Expected: void
98
+ Got: #{actual_md.return_types.to_sig}
99
+ END
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,10 @@
1
+ module GelautoSpecs
2
+ class System
3
+ def self.configure(config)
4
+ @config = config
5
+ nil
6
+ end
7
+ end
8
+ end
9
+
10
+ Gelauto.paths << __FILE__
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gelauto
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Cameron Dutro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-07-03 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.6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: gli
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ description: Automatically annotate your code with Sorbet type definitions.
42
+ email:
43
+ - camertron@gmail.com
44
+ executables:
45
+ - gelauto
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - Gemfile
50
+ - README.md
51
+ - Rakefile
52
+ - bin/gelauto
53
+ - gelauto.gemspec
54
+ - lib/gelauto.rb
55
+ - lib/gelauto/arg_list.rb
56
+ - lib/gelauto/array_type.rb
57
+ - lib/gelauto/boolean_type.rb
58
+ - lib/gelauto/cli_utils.rb
59
+ - lib/gelauto/generic_type.rb
60
+ - lib/gelauto/hash_type.rb
61
+ - lib/gelauto/logger.rb
62
+ - lib/gelauto/method_def.rb
63
+ - lib/gelauto/method_index.rb
64
+ - lib/gelauto/namespace.rb
65
+ - lib/gelauto/rbi.rb
66
+ - lib/gelauto/rspec.rb
67
+ - lib/gelauto/type.rb
68
+ - lib/gelauto/type_set.rb
69
+ - lib/gelauto/utils.rb
70
+ - lib/gelauto/var.rb
71
+ - lib/gelauto/version.rb
72
+ - spec/gelauto_spec.rb
73
+ - spec/spec_helper.rb
74
+ - spec/support/client.rb
75
+ - spec/support/config.yml
76
+ - spec/support/image.rb
77
+ - spec/support/matchers.rb
78
+ - spec/support/system.rb
79
+ homepage: http://github.com/camertron/gelauto
80
+ licenses: []
81
+ metadata: {}
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubygems_version: 3.0.4
98
+ signing_key:
99
+ specification_version: 4
100
+ summary: Automatically annotate your code with Sorbet type definitions.
101
+ test_files: []