sorbet-cfg 0.1.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: 48c8482457e5a75f4cc1ecffb77c1fbeb9c84b7d6ca875321ebcea98d4ef93d1
4
+ data.tar.gz: 56a087dffb2f6007debd11e60b03492344a866ce1b9af55d2a8f1da4164cda16
5
+ SHA512:
6
+ metadata.gz: 199335bf741fe283f281fda0c79d6299b82207f0098814c32321883e721fed2df24926108c62568049380d4fb2635627d1b6eaacc2383d591a25ec8867f3a827
7
+ data.tar.gz: 44eeff0c0185f951cbea1133e9aba24c839c81294a82223a5160fb5974a92d05ae6b706831134c68dad1dd0110cc71682130c9a45417882ed9081b4354e2009a
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in sorbet-cfg.gemspec
4
+ gemspec
5
+
6
+ gem "parslet", "~> 1.8"
7
+
8
+ gem "rspec", "~> 3.9"
9
+
10
+ gem "sorbet-runtime", "~> 0.4.5005"
data/Gemfile.lock ADDED
@@ -0,0 +1,43 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sorbet-cfg (0.1.0)
5
+ sorbet
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ diff-lcs (1.3)
11
+ parslet (1.8.2)
12
+ rake (10.5.0)
13
+ rspec (3.9.0)
14
+ rspec-core (~> 3.9.0)
15
+ rspec-expectations (~> 3.9.0)
16
+ rspec-mocks (~> 3.9.0)
17
+ rspec-core (3.9.0)
18
+ rspec-support (~> 3.9.0)
19
+ rspec-expectations (3.9.0)
20
+ diff-lcs (>= 1.2.0, < 2.0)
21
+ rspec-support (~> 3.9.0)
22
+ rspec-mocks (3.9.0)
23
+ diff-lcs (>= 1.2.0, < 2.0)
24
+ rspec-support (~> 3.9.0)
25
+ rspec-support (3.9.0)
26
+ sorbet (0.4.4997)
27
+ sorbet-static (= 0.4.4997)
28
+ sorbet-runtime (0.4.5005)
29
+ sorbet-static (0.4.4997-universal-darwin-14)
30
+
31
+ PLATFORMS
32
+ ruby
33
+
34
+ DEPENDENCIES
35
+ bundler (~> 2.0)
36
+ parslet (~> 1.8)
37
+ rake (~> 10.0)
38
+ rspec (~> 3.9)
39
+ sorbet-cfg!
40
+ sorbet-runtime (~> 0.4.5005)
41
+
42
+ BUNDLED WITH
43
+ 2.0.2
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Aaron Christiansen
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,39 @@
1
+ # Sorbet::Cfg
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/sorbet/cfg`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'sorbet-cfg'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install sorbet-cfg
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ 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).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/AaronC81/sorbet-cfg.
36
+
37
+ ## License
38
+
39
+ 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,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,132 @@
1
+ # typed: true
2
+
3
+ require 'pathname'
4
+ require 'open3'
5
+ require 'json'
6
+
7
+ module SorbetCFG
8
+ module Loader
9
+ extend T::Sig
10
+
11
+ sig { returns(T::Hash[T.any(Module, Class), T::Hash[String, Tree::CFG]]) }
12
+ def self.index
13
+ @index ||= {}
14
+ end
15
+
16
+ sig { params(a: Integer, b: Integer).returns(Integer) }
17
+ def self.foo(a, b)
18
+ a + b
19
+ end
20
+
21
+ sig { params(path: String).returns(T.nilable(String)) }
22
+ ##
23
+ # Given an ABSOLUTE path to somewhere within a Sorbet-enabled Ruby project,
24
+ # finds the root of the project by looking for a Gemfile or .bundle, as well
25
+ # as a sorbet directory.
26
+ def self.project_root(path)
27
+ # TODO: Sorbet is currently Unix-only, so this code is too
28
+
29
+ raise 'path does not appear to be absolute' unless path.start_with?('/')
30
+
31
+ possible_paths = []
32
+ Pathname.new(path).each_filename do |part|
33
+ possible_paths << (possible_paths.empty? \
34
+ ? "/#{part}"
35
+ : File.join(possible_paths.last, part))
36
+ end
37
+
38
+ possible_paths.reverse.each do |possible_path|
39
+ has_bundler = File.exist?(File.join(possible_path, 'Gemfile')) ||
40
+ File.exist?(File.join(possible_path, '.bundle'))
41
+
42
+ has_sorbet = File.exist?(File.join(possible_path, 'sorbet'))
43
+
44
+ return possible_path if has_bundler && has_sorbet
45
+ end
46
+
47
+ nil
48
+ end
49
+
50
+ sig { params(target: T.any(Module, Class)).returns(String) }
51
+ ##
52
+ # Given a module or class, returns the contents of an RBI file which, when
53
+ # loaded, makes that module or class extend T::CFGExport so that Sorbet's
54
+ # "-p cfg-json" option will print JSON for its CFG.
55
+ def self.generate_cfg_export_rbi(target)
56
+ result_lines = []
57
+
58
+ # Start from the top
59
+ current_object = T.let(Kernel, T.untyped)
60
+
61
+ total_nesting = 0
62
+ target.to_s.split('::').each.with_index do |part_name, i|
63
+ current_object = current_object.const_get(part_name)
64
+
65
+ if current_object.is_a?(Class)
66
+ kind = "class"
67
+ elsif current_object.is_a?(Module)
68
+ kind = "module"
69
+ else
70
+ raise "#{current_object} is neither a class nor a module"
71
+ end
72
+
73
+ result_lines << "#{' ' * i}#{kind} #{part_name}"
74
+ total_nesting = i
75
+ end
76
+
77
+ result_lines << "#{' ' * (total_nesting + 1)}extend T::CFGExport"
78
+
79
+ (total_nesting + 1).times do |i|
80
+ this_level = total_nesting - i
81
+ result_lines << "#{' ' * this_level}end"
82
+ end
83
+
84
+ result_lines.join("\n") + "\n"
85
+ end
86
+
87
+ sig { params(obj: Module).void }
88
+ def self.index_module(obj)
89
+ rbi_contents = generate_cfg_export_rbi(obj)
90
+ rbi_path = '/tmp/sorbet-cfg-export.rbi'
91
+ File.write(rbi_path, rbi_contents)
92
+
93
+ result = T.let(nil, T.nilable(Tree::MultiCFG))
94
+
95
+ all_methods = obj.methods + obj.instance_methods
96
+ raise "can\'t get the source location of #{obj} because it contains no methods" if all_methods.empty?
97
+
98
+ # Just pick the first one, it's easiest
99
+ # TODO: might be best to pick all unique, though?
100
+ target = obj.method(T.must(all_methods[0]))
101
+ project = project_root(Utilities.true_source_location(target)[0])
102
+
103
+ raise 'unable to locate project root' unless project
104
+
105
+ Open3.popen3('srb', 'tc', rbi_path, '-p', 'cfg-json', chdir: project) do |i, o, e, t|
106
+ json_docs = o.read
107
+
108
+ # TODO: VERY BAD
109
+ # Just all of this is terrible
110
+ # There's a stub element at the end because trailing commas aren't valid JSON
111
+ cursed_json_doc = "[#{json_docs.gsub(/^\}/, '},')} null]"
112
+ parsed_json = JSON.parse(cursed_json_doc)[0...-1]
113
+ result = Tree::MultiCFG.new(cfg: parsed_json.map { |x| Tree::CFG.from_hash(x) })
114
+ end
115
+
116
+ raise "indexing failed for #{obj}" unless result
117
+ index[obj] = {}
118
+ result.cfg.each do |cfg|
119
+ if cfg.definition_full_name.include?('.')
120
+ prefix_char = '.'
121
+ elsif cfg.definition_full_name.include?('#')
122
+ prefix_char = '#'
123
+ else
124
+ raise "unable to determine type of #{cfg}"
125
+ end
126
+
127
+ def_name = "#{prefix_char}#{cfg.definition_full_name.split(prefix_char).last}"
128
+ T.must(index[obj])[def_name] = cfg
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,37 @@
1
+ # typed: ignore
2
+ require 'parslet'
3
+
4
+ module SorbetCFG
5
+ class RawParser < Parslet::Parser
6
+ # As far as I can tell, it's OK to just shotgun most operators, but < and >
7
+ # need special care because they delimit #sorbet_name
8
+ # There's probably some absolutely horrendous stuff that this accepts, but
9
+ # if that sort of stuff occurs in a CFG, we have bigger problems
10
+ rule(:name) { match('[A-Za-z_0-9=\-|&.!~?]').repeat(1) |
11
+ (str('<') >> name >> str('>') | # Matches <=> too!
12
+ str('>') | str('<') | str('<=') | str('>=')) }
13
+
14
+ rule(:space) { match('\s').repeat(1) }
15
+ rule(:space?) { space.maybe }
16
+
17
+ rule(:langle) { space? >> match('<') >> space? }
18
+ rule(:rangle) { space? >> match('>') >> space? }
19
+
20
+ rule(:lbrace) { space? >> match('{') >> space? }
21
+ rule(:rbrace) { space? >> match('}') >> space? }
22
+
23
+ rule(:sorbet_name) { langle >>
24
+ match('[A-Z]').repeat(1).as(:kind) >>
25
+ space? >>
26
+ name.as(:value) >>
27
+ rangle }
28
+
29
+ rule(:local_var) { sorbet_name.as(:name) >>
30
+ str('$') >>
31
+ match('[0-9]').repeat(1).as(:unique) >>
32
+ space? }
33
+
34
+ rule(:node) { identifier.as(:type) >> lbrace >> rbrace }
35
+ root(:node)
36
+ end
37
+ end
@@ -0,0 +1,97 @@
1
+ # typed: true
2
+ require_relative 'loc'
3
+ require_relative 'type'
4
+
5
+ module SorbetCFG
6
+ module Tree
7
+ module Instruction; end
8
+
9
+ class LocalVariable < T::Struct
10
+ prop :unique_name, String
11
+ prop :type, Type
12
+ end
13
+
14
+ class Block < T::Struct
15
+ class BlockExit < T::Struct
16
+ prop :cond, T.nilable(LocalVariable)
17
+ prop :then_block, Integer
18
+ prop :else_block, Integer
19
+ prop :location, Loc
20
+ end
21
+
22
+ prop :id, T.nilable(Integer)
23
+ prop :bindings, T::Array[Binding], factory: ->{ [] }
24
+ prop :block_exit, BlockExit
25
+ end
26
+
27
+ class UnknownInstruction < T::Struct
28
+ extend Instruction
29
+ end
30
+
31
+ class IdentInstruction < T::Struct
32
+ extend Instruction
33
+ prop :ident, LocalVariable
34
+ end
35
+
36
+ class AliasInstruction < T::Struct
37
+ extend Instruction
38
+ prop :alias_full_name, String
39
+ end
40
+
41
+ class SendInstruction < T::Struct
42
+ extend Instruction
43
+ prop :receiver, LocalVariable
44
+ prop :method_name, String
45
+ prop :arguments, T::Array[LocalVariable]
46
+ prop :block, Block
47
+ end
48
+
49
+ class ReturnInstruction < T::Struct
50
+ extend Instruction
51
+ prop :return, LocalVariable
52
+ end
53
+
54
+ class LiteralInstruction < T::Struct
55
+ extend Instruction
56
+ prop :literal, Type
57
+ end
58
+
59
+ class UnanalyzableType < T::Struct
60
+ extend Instruction
61
+ end
62
+
63
+ class LoadArgType < T::Struct
64
+ extend Instruction
65
+ prop :load_arg_name, String
66
+ end
67
+
68
+ class CastInstruction < T::Struct
69
+ extend Instruction
70
+ prop :value, LocalVariable
71
+ prop :type, Type
72
+ end
73
+
74
+ class Binding < T::Struct
75
+ prop :bind, LocalVariable
76
+ prop :value, Instruction
77
+ prop :location, Loc
78
+ end
79
+
80
+ class CFG < T::Struct
81
+ class Argument < T::Struct
82
+ prop :name, String
83
+ prop :type, T.nilable(Type)
84
+ end
85
+
86
+ prop :definition_full_name, String
87
+ prop :location, Loc
88
+ prop :return_type, T.nilable(Type)
89
+ prop :arguments, T::Array[Argument]
90
+ prop :blocks, T::Array[Block]
91
+ end
92
+
93
+ class MultiCFG < T::Struct
94
+ prop :cfg, T::Array[CFG]
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,20 @@
1
+ # typed: true
2
+
3
+ module SorbetCFG
4
+ module Tree
5
+ class Loc < T::Struct
6
+ class Position < T::Struct
7
+ class LC < T::Struct
8
+ prop :line, T.nilable(Integer)
9
+ prop :column, T.nilable(Integer)
10
+ end
11
+
12
+ prop :start, LC
13
+ prop :end, LC
14
+ end
15
+
16
+ prop :path, String
17
+ prop :position, Position
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,52 @@
1
+ # typed: true
2
+
3
+ module SorbetCFG
4
+ module Tree
5
+ module Type; end
6
+
7
+ class UnknownType < T::Struct
8
+ extend Type
9
+ end
10
+
11
+ class ClassType < T::Struct
12
+ extend Type
13
+ prop :class_full_name, String
14
+ end
15
+
16
+ class CompositeType < T::Struct
17
+ class Operator < T::Enum
18
+ enums do
19
+ OR = new
20
+ AND = new
21
+ end
22
+ end
23
+
24
+ extend Type
25
+ prop :left, Type
26
+ prop :right, Type
27
+ prop :operator, Operator
28
+ end
29
+
30
+ class AppliedType < T::Struct
31
+ extend Type
32
+ prop :symbol_full_name, String
33
+ prop :type_args, T::Array[Type]
34
+ end
35
+
36
+ class ShapeType < T::Struct
37
+ extend Type
38
+ prop :keys, T::Array[Type]
39
+ prop :values, T::Array[Type]
40
+ end
41
+
42
+ class LiteralType < T::Struct
43
+ extend Type
44
+ prop :value, T.any(Integer, String, Symbol, T::Boolean, Float)
45
+ end
46
+
47
+ class TupleType < T::Struct
48
+ extend Type
49
+ prop :elems, T::Array[Type]
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,28 @@
1
+ # typed: true
2
+
3
+ module SorbetCFG
4
+ module Utilities
5
+ extend T::Sig
6
+
7
+ sig { params(meth: Method).returns([String, Integer]) }
8
+ def self.true_source_location(meth)
9
+ loc = meth.source_location
10
+
11
+ file_path, line = loc
12
+ return loc unless file_path && File.exists?(file_path)
13
+
14
+ first_source_line = IO.readlines(file_path)[line - 1]
15
+
16
+ # This is how Sorbet replaces methods.
17
+ # If Sorbet undergoes drastic refactorings, this may need to be updated!
18
+ initial_sorbet_line = "T::Private::ClassUtils.replace_method(mod, method_name) do |*args, &blk|"
19
+ replaced_sorbet_line = "mod.send(:define_method, method_sig.method_name) do |*args, &blk|"
20
+
21
+ if [initial_sorbet_line, replaced_sorbet_line].include?(T.must(first_source_line).strip)
22
+ T::Private::Methods.signature_for_method(meth).method.source_location
23
+ else
24
+ loc
25
+ end
26
+ end
27
+ end
28
+ end
data/lib/sorbet-cfg.rb ADDED
@@ -0,0 +1,13 @@
1
+ # typed: true
2
+
3
+ require 'sorbet-runtime'
4
+
5
+ require_relative 'sorbet-cfg/utilities'
6
+
7
+ require_relative 'sorbet-cfg/tree/loc'
8
+ require_relative 'sorbet-cfg/tree/type'
9
+ require_relative 'sorbet-cfg/tree/cfg'
10
+
11
+ require_relative 'sorbet-cfg/raw_parser'
12
+
13
+ require_relative 'sorbet-cfg/loader'
data/sorbet/config ADDED
@@ -0,0 +1,2 @@
1
+ --dir
2
+ .