orbacle 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 +4 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +119 -0
- data/LICENSE +22 -0
- data/Makefile +57 -0
- data/README.md +53 -0
- data/circle.yml +82 -0
- data/exe/orbaclerun +6 -0
- data/index.html +106 -0
- data/lib/orbacle.rb +96 -0
- data/lib/orbacle/ast_utils.rb +35 -0
- data/lib/orbacle/bottom_type.rb +23 -0
- data/lib/orbacle/builder.rb +1414 -0
- data/lib/orbacle/builder/context.rb +71 -0
- data/lib/orbacle/builder/operator_assignment_processors.rb +80 -0
- data/lib/orbacle/class_type.rb +32 -0
- data/lib/orbacle/command_line_interface.rb +107 -0
- data/lib/orbacle/const_name.rb +33 -0
- data/lib/orbacle/const_ref.rb +53 -0
- data/lib/orbacle/constants_tree.rb +73 -0
- data/lib/orbacle/define_builtins.rb +139 -0
- data/lib/orbacle/engine.rb +74 -0
- data/lib/orbacle/find_definition_under_position.rb +76 -0
- data/lib/orbacle/generic_type.rb +35 -0
- data/lib/orbacle/global_tree.rb +280 -0
- data/lib/orbacle/graph.rb +126 -0
- data/lib/orbacle/indexer.rb +151 -0
- data/lib/orbacle/integer_id_generator.rb +13 -0
- data/lib/orbacle/lambda_type.rb +37 -0
- data/lib/orbacle/lang_server.rb +64 -0
- data/lib/orbacle/main_type.rb +23 -0
- data/lib/orbacle/nesting.rb +78 -0
- data/lib/orbacle/node.rb +23 -0
- data/lib/orbacle/nominal_type.rb +32 -0
- data/lib/orbacle/ruby_parser.rb +19 -0
- data/lib/orbacle/scope.rb +63 -0
- data/lib/orbacle/selfie.rb +41 -0
- data/lib/orbacle/type_pretty_printer.rb +24 -0
- data/lib/orbacle/typing_service.rb +816 -0
- data/lib/orbacle/union_type.rb +40 -0
- data/lib/orbacle/uuid_id_generator.rb +11 -0
- data/lib/orbacle/worklist.rb +51 -0
- data/orbacle.gemspec +33 -0
- metadata +258 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Orbacle
|
4
|
+
class Builder
|
5
|
+
class Context
|
6
|
+
AnalyzedKlass = Struct.new(:klass_id, :method_visibility)
|
7
|
+
|
8
|
+
def initialize(filepath, selfie, nesting, analyzed_klass, analyzed_method, lenv)
|
9
|
+
@filepath = filepath.freeze
|
10
|
+
@selfie = selfie.freeze
|
11
|
+
@nesting = nesting.freeze
|
12
|
+
@analyzed_klass = analyzed_klass.freeze
|
13
|
+
@analyzed_method = analyzed_method.freeze
|
14
|
+
@lenv = lenv.freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :filepath, :selfie, :nesting, :analyzed_klass, :analyzed_method, :lenv
|
18
|
+
|
19
|
+
def with_selfie(new_selfie)
|
20
|
+
self.class.new(filepath, new_selfie, nesting, analyzed_klass, analyzed_method, lenv)
|
21
|
+
end
|
22
|
+
|
23
|
+
def with_nesting(new_nesting)
|
24
|
+
self.class.new(filepath, selfie, new_nesting, analyzed_klass, analyzed_method, lenv)
|
25
|
+
end
|
26
|
+
|
27
|
+
def scope
|
28
|
+
nesting.to_scope
|
29
|
+
end
|
30
|
+
|
31
|
+
def with_analyzed_klass(new_klass_id)
|
32
|
+
self.class.new(filepath, selfie, nesting, AnalyzedKlass.new(new_klass_id, :public), analyzed_method, lenv)
|
33
|
+
end
|
34
|
+
|
35
|
+
def with_visibility(new_visibility)
|
36
|
+
self.class.new(filepath, selfie, nesting, AnalyzedKlass.new(analyzed_klass.klass_id, new_visibility), analyzed_method, lenv)
|
37
|
+
end
|
38
|
+
|
39
|
+
def with_analyzed_method(new_analyzed_method_id)
|
40
|
+
self.class.new(filepath, selfie, nesting, analyzed_klass, new_analyzed_method_id, lenv)
|
41
|
+
end
|
42
|
+
|
43
|
+
def merge_lenv(new_lenv)
|
44
|
+
self.class.new(filepath, selfie, nesting, analyzed_klass, analyzed_method, lenv.merge(new_lenv))
|
45
|
+
end
|
46
|
+
|
47
|
+
def lenv_fetch(key)
|
48
|
+
lenv.fetch(key, [])
|
49
|
+
end
|
50
|
+
|
51
|
+
def with_lenv(new_lenv)
|
52
|
+
self.class.new(filepath, selfie, nesting, analyzed_klass, analyzed_method, new_lenv)
|
53
|
+
end
|
54
|
+
|
55
|
+
def analyzed_klass_id
|
56
|
+
analyzed_klass.klass_id
|
57
|
+
end
|
58
|
+
|
59
|
+
def with_merged_lenvs(lenv1, lenv2)
|
60
|
+
final_lenv = {}
|
61
|
+
|
62
|
+
var_names = (lenv1.keys + lenv2.keys).uniq
|
63
|
+
var_names.each do |var_name|
|
64
|
+
final_lenv[var_name] = (lenv1.fetch(var_name, []) + lenv2.fetch(var_name, [])).uniq
|
65
|
+
end
|
66
|
+
|
67
|
+
with_lenv(final_lenv)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Orbacle
|
4
|
+
class Builder
|
5
|
+
module OperatorAssignmentProcessors
|
6
|
+
def handle_op_asgn(ast, context)
|
7
|
+
partial_assignment_ast, operator_name, argument_ast = ast.children
|
8
|
+
|
9
|
+
return process(
|
10
|
+
complete_assignment(
|
11
|
+
partial_assignment_ast,
|
12
|
+
Parser::AST::Node.new(:send, [
|
13
|
+
build_accessor_based_on_assignment(partial_assignment_ast),
|
14
|
+
operator_name,
|
15
|
+
argument_ast])),
|
16
|
+
context)
|
17
|
+
end
|
18
|
+
|
19
|
+
def handle_or_asgn(ast, context)
|
20
|
+
partial_assignment_ast, argument_ast = ast.children
|
21
|
+
|
22
|
+
return process(
|
23
|
+
complete_assignment(
|
24
|
+
partial_assignment_ast,
|
25
|
+
Parser::AST::Node.new(:or, [
|
26
|
+
build_accessor_based_on_assignment(partial_assignment_ast),
|
27
|
+
argument_ast])),
|
28
|
+
context)
|
29
|
+
end
|
30
|
+
|
31
|
+
def handle_and_asgn(ast, context)
|
32
|
+
partial_assignment_ast, argument_ast = ast.children
|
33
|
+
|
34
|
+
return process(
|
35
|
+
complete_assignment(
|
36
|
+
partial_assignment_ast,
|
37
|
+
Parser::AST::Node.new(:and, [
|
38
|
+
build_accessor_based_on_assignment(partial_assignment_ast),
|
39
|
+
argument_ast])),
|
40
|
+
context)
|
41
|
+
end
|
42
|
+
|
43
|
+
def build_accessor_based_on_assignment(assignment_ast)
|
44
|
+
case assignment_ast.type
|
45
|
+
when :lvasgn
|
46
|
+
var_name = assignment_ast.children[0]
|
47
|
+
Parser::AST::Node.new(:lvar, [var_name])
|
48
|
+
when :ivasgn
|
49
|
+
var_name = assignment_ast.children[0]
|
50
|
+
Parser::AST::Node.new(:ivar, [var_name])
|
51
|
+
when :cvasgn
|
52
|
+
var_name = assignment_ast.children[0]
|
53
|
+
Parser::AST::Node.new(:cvar, [var_name])
|
54
|
+
when :casgn
|
55
|
+
scope = assignment_ast.children[0]
|
56
|
+
var_name = assignment_ast.children[1]
|
57
|
+
Parser::AST::Node.new(:const, [scope, var_name])
|
58
|
+
when :send
|
59
|
+
send_obj = assignment_ast.children[0]
|
60
|
+
asgn_method_name = assignment_ast.children[1]
|
61
|
+
args = assignment_ast.children[2..-1]
|
62
|
+
Parser::AST::Node.new(:send, [send_obj, asgn_method_name, *args])
|
63
|
+
when :gvasgn
|
64
|
+
var_name = assignment_ast.children[0]
|
65
|
+
Parser::AST::Node.new(:gvar, [var_name])
|
66
|
+
else raise ArgumentError
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def complete_assignment(partial_assignment_ast, full_rhs_ast)
|
71
|
+
if partial_assignment_ast.type == :send
|
72
|
+
send_obj_ast, accessor_method_name, _ = partial_assignment_ast.children
|
73
|
+
partial_assignment_ast.updated(nil, [send_obj_ast, "#{accessor_method_name}=", full_rhs_ast])
|
74
|
+
elsif [:lvasgn, :ivasgn, :cvasgn, :casgn, :gvasgn].include?(partial_assignment_ast.type)
|
75
|
+
partial_assignment_ast.append(full_rhs_ast)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Orbacle
|
4
|
+
class ClassType
|
5
|
+
def initialize(name)
|
6
|
+
@name = name
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
self.class == other.class &&
|
13
|
+
self.name == other.name
|
14
|
+
end
|
15
|
+
|
16
|
+
def hash
|
17
|
+
[
|
18
|
+
self.class,
|
19
|
+
self.name,
|
20
|
+
].hash ^ BIG_VALUE
|
21
|
+
end
|
22
|
+
alias eql? ==
|
23
|
+
|
24
|
+
def each_possible_type
|
25
|
+
yield self
|
26
|
+
end
|
27
|
+
|
28
|
+
def bottom?
|
29
|
+
false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'pathname'
|
5
|
+
require 'logger'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'orbacle'
|
8
|
+
require 'lsp'
|
9
|
+
require 'json'
|
10
|
+
|
11
|
+
module Orbacle
|
12
|
+
class CommandLineInterface
|
13
|
+
class Options
|
14
|
+
def initialize
|
15
|
+
@dir = Dir.pwd
|
16
|
+
@stats_file = Pathname.new(Dir.pwd).join("stats.json")
|
17
|
+
end
|
18
|
+
attr_reader :dir, :stats_file
|
19
|
+
|
20
|
+
def define_options(parser)
|
21
|
+
parser.banner = 'Usage: ./orbacle [options]'
|
22
|
+
|
23
|
+
parser.on('-d DIR', '--dir', 'Directory in which project resides') do |dir|
|
24
|
+
@dir = dir
|
25
|
+
end
|
26
|
+
|
27
|
+
parser.on("-h", "--help", "Prints this help") do
|
28
|
+
puts parser
|
29
|
+
exit
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def call(args)
|
35
|
+
options = Options.new
|
36
|
+
OptionParser.new do |parser|
|
37
|
+
options.define_options(parser)
|
38
|
+
end.parse!(args)
|
39
|
+
call_command(args[0], options)
|
40
|
+
end
|
41
|
+
|
42
|
+
def call_command(command, options)
|
43
|
+
case command
|
44
|
+
when 'index' then index(options)
|
45
|
+
when 'file-server' then file_server(options)
|
46
|
+
when 'generate-datajs' then generate_datajs(options)
|
47
|
+
else no_command
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def index(options)
|
54
|
+
logger = Logger.new(STDOUT)
|
55
|
+
project_root = options.dir
|
56
|
+
|
57
|
+
engine = Engine.new(logger)
|
58
|
+
engine.index(project_root)
|
59
|
+
ensure
|
60
|
+
File.open(options.stats_file, "w") {|f| f.write(engine.stats_recorder.all_stats.to_json) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def file_server(options)
|
64
|
+
logger = Logger.new('/tmp/orbacle.log', 'monthly')
|
65
|
+
logger.level = Logger::INFO
|
66
|
+
|
67
|
+
engine = Engine.new(logger)
|
68
|
+
lang_server = LangServer.new(logger, engine)
|
69
|
+
server = Lsp::FileLanguageServer.new(lang_server, logger: logger)
|
70
|
+
server.start
|
71
|
+
end
|
72
|
+
|
73
|
+
def generate_datajs(options)
|
74
|
+
logger = Logger.new(STDOUT)
|
75
|
+
|
76
|
+
require 'base64'
|
77
|
+
project_root = options.dir
|
78
|
+
engine = Engine.new(logger)
|
79
|
+
tree, typing_result, graph = engine.index(project_root)
|
80
|
+
|
81
|
+
nodes = graph.vertices
|
82
|
+
filepaths = nodes.map {|n| n.location&.uri }.compact.uniq
|
83
|
+
type_pretty_printer = TypePrettyPrinter.new
|
84
|
+
|
85
|
+
File.open("data.js", "w") do |f|
|
86
|
+
f.puts "window.orbacleFiles = ["
|
87
|
+
filepaths.each do |filepath|
|
88
|
+
f.puts " ['#{filepath[project_root.to_s.size..-1]}', `#{Base64.encode64(File.read(filepath))}`],"
|
89
|
+
end
|
90
|
+
f.puts "];"
|
91
|
+
f.puts "window.orbacleNodes = ["
|
92
|
+
sorted_nodes = nodes.reject {|n| n.location&.uri.nil? }
|
93
|
+
sorted_nodes.each do |node|
|
94
|
+
filepath = node.location.uri[project_root.to_s.size..-1]
|
95
|
+
pretty_type = type_pretty_printer.(typing_result[node])
|
96
|
+
f.puts "['#{node.type}', '#{pretty_type}', '#{filepath}', #{node.location&.start&.line&.to_i}, #{node.location&.start&.character&.to_i}, #{node.location&.end&.line&.to_i}, #{node.location&.end&.character&.to_i}],"
|
97
|
+
end
|
98
|
+
f.puts "];"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def no_command
|
103
|
+
puts "No command given."
|
104
|
+
exit
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Orbacle
|
4
|
+
class ConstName
|
5
|
+
def self.from_string(str)
|
6
|
+
raise ArgumentError if str.start_with?("::")
|
7
|
+
new(str.split("::"))
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(elems)
|
11
|
+
@elems = elems
|
12
|
+
raise ArgumentError if elems.empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :elems
|
16
|
+
|
17
|
+
def ==(other)
|
18
|
+
elems == other.elems
|
19
|
+
end
|
20
|
+
|
21
|
+
def name
|
22
|
+
elems.last
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_string
|
26
|
+
elems.join("::")
|
27
|
+
end
|
28
|
+
|
29
|
+
def scope
|
30
|
+
Scope.new(elems[0..-2], false)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Orbacle
|
4
|
+
class ConstRef
|
5
|
+
def self.from_ast(ast, nesting)
|
6
|
+
full_name = AstUtils.const_to_string(ast)
|
7
|
+
from_full_name(full_name, nesting)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.from_full_name(full_name, nesting)
|
11
|
+
if full_name.start_with?("::")
|
12
|
+
name = full_name[2..-1]
|
13
|
+
new(ConstName.from_string(name), true, nesting)
|
14
|
+
else
|
15
|
+
new(ConstName.from_string(full_name), false, nesting)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(const_name, is_absolute, nesting)
|
20
|
+
@const_name = const_name
|
21
|
+
@is_absolute = is_absolute
|
22
|
+
@nesting = nesting
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :const_name, :is_absolute, :nesting
|
26
|
+
|
27
|
+
def to_full_const_name
|
28
|
+
if absolute?
|
29
|
+
const_name
|
30
|
+
else
|
31
|
+
ConstName.new([*nesting.to_primitive, const_name.to_string])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def absolute?
|
36
|
+
@is_absolute
|
37
|
+
end
|
38
|
+
|
39
|
+
def relative_name
|
40
|
+
const_name.to_string
|
41
|
+
end
|
42
|
+
|
43
|
+
def name
|
44
|
+
const_name.name
|
45
|
+
end
|
46
|
+
|
47
|
+
def ==(other)
|
48
|
+
const_name == other.const_name &&
|
49
|
+
is_absolute == other.is_absolute &&
|
50
|
+
nesting == other.nesting
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Orbacle
|
4
|
+
class ConstantsTree
|
5
|
+
ScopeLevel = Struct.new(:elements, :children) do
|
6
|
+
def self.empty
|
7
|
+
new([], build_empty_hash)
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.build_empty_hash
|
11
|
+
Hash.new {|h, k| h[k] = ScopeLevel.empty }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@tree = ScopeLevel.build_empty_hash
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_element(scope, name, element)
|
20
|
+
current_children = @tree
|
21
|
+
scope.elems.each do |scope_level|
|
22
|
+
current_children = current_children[scope_level].children
|
23
|
+
end
|
24
|
+
current_children[name].elements << element
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_by_const_name(const_name)
|
28
|
+
scope_children = children_of_scope(const_name.scope)
|
29
|
+
scope_children[const_name.name].elements.first
|
30
|
+
end
|
31
|
+
|
32
|
+
def select_by_const_ref(const_ref)
|
33
|
+
nesting = const_ref.nesting
|
34
|
+
while !nesting.empty?
|
35
|
+
result = select_by_scope_and_name(nesting.to_scope.increase_by_ref(const_ref).decrease, const_ref.name)
|
36
|
+
return result if !result.empty?
|
37
|
+
nesting = nesting.decrease_nesting
|
38
|
+
end
|
39
|
+
select_by_scope_and_name(Scope.empty.increase_by_ref(const_ref).decrease, const_ref.name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_by_const_ref(const_ref)
|
43
|
+
select_by_const_ref(const_ref).first
|
44
|
+
end
|
45
|
+
|
46
|
+
def find(&block)
|
47
|
+
find_in_children(@tree, &block)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def select_by_scope_and_name(scope, name)
|
52
|
+
scope_level = children_of_scope(scope)[name]
|
53
|
+
scope_level.elements
|
54
|
+
end
|
55
|
+
|
56
|
+
def children_of_scope(scope)
|
57
|
+
scope.elems.reduce(@tree) do |current_scope_level, scope_elem|
|
58
|
+
current_scope_level[scope_elem].children
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def find_in_children(children, &block)
|
63
|
+
children.each do |_child_name, child_level|
|
64
|
+
child_level.elements.each do |constant|
|
65
|
+
return constant if block.call(constant)
|
66
|
+
end
|
67
|
+
result_in_child_level = find_in_children(child_level.children, &block)
|
68
|
+
return result_in_child_level if result_in_child_level
|
69
|
+
end
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|