orbacle 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|