solargraph-arc 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
1
+ module Solargraph
2
+ module Arc
3
+ class Delegate
4
+ def self.instance
5
+ @instance ||= self.new
6
+ end
7
+
8
+ def process(source_map, ns)
9
+ return [] unless source_map.code.include?("delegate")
10
+
11
+ walker = Walker.from_source(source_map.source)
12
+ pins = []
13
+
14
+ walker.on :send, [nil, :delegate] do |ast|
15
+ methods = ast.children[2..-1]
16
+ .map {|c| c.children.first }
17
+ .select {|s| s.is_a?(Symbol) }
18
+
19
+ methods.each do |meth|
20
+ pins << Util.build_public_method(
21
+ ns,
22
+ meth.to_s,
23
+ location: Util.build_location(ast, ns.filename)
24
+ )
25
+ end
26
+ end
27
+
28
+ walker.walk
29
+ Solargraph.logger.debug("[ARC][Delegate] added #{pins.map(&:name)} to #{ns.path}")
30
+ pins
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,94 @@
1
+ module Solargraph
2
+ module Arc
3
+ class Devise
4
+ def self.instance
5
+ @instance ||= self.new
6
+ end
7
+
8
+ def initialize
9
+ @seen_devise_closures = []
10
+ end
11
+
12
+ def process(source_map, ns)
13
+ if source_map.filename.include?("app/models")
14
+ process_model(source_map, ns)
15
+ elsif source_map.filename.end_with?("app/controllers/application_controller.rb")
16
+ process_controller(source_map, ns)
17
+ else
18
+ []
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def process_model(source_map, ns)
25
+ walker = Walker.from_source(source_map.source)
26
+ pins = []
27
+
28
+ walker.on :send, [nil, :devise] do |ast|
29
+ @seen_devise_closures << ns
30
+
31
+ modules = ast.children[2..-1]
32
+ .map {|c| c.children.first }
33
+ .select {|s| s.is_a?(Symbol) }
34
+
35
+ modules.each do |mod|
36
+ pins << Util.build_module_include(
37
+ ns,
38
+ "Devise::Models::#{mod.to_s.capitalize}",
39
+ Util.build_location(ast, ns.filename)
40
+ )
41
+ end
42
+ end
43
+
44
+ walker.walk
45
+ Solargraph.logger.debug("[ARC][Devise] added #{pins.map(&:name)} to #{ns.path}") if pins.any?
46
+ pins
47
+ end
48
+
49
+ def process_controller(source_map, ns)
50
+ pins = [
51
+ Util.build_module_include(
52
+ ns,
53
+ "Devise::Controllers::Helpers",
54
+ Util.dummy_location(ns.filename)
55
+ )
56
+ ]
57
+
58
+ mapping_pins = @seen_devise_closures.map do |model_ns|
59
+ ast = Walker.normalize_ast(source_map.source)
60
+ mapping = model_ns.name.underscore
61
+
62
+ [
63
+ Util.build_public_method(
64
+ ns,
65
+ "authenticate_#{mapping}!",
66
+ location: Util.build_location(ast, ns.filename)
67
+ ),
68
+ Util.build_public_method(
69
+ ns,
70
+ "#{mapping}_signed_in?",
71
+ types: ["true", "false"],
72
+ location: Util.build_location(ast, ns.filename)
73
+ ),
74
+ Util.build_public_method(
75
+ ns,
76
+ "current_#{mapping}",
77
+ types: [model_ns.name, "nil"],
78
+ location: Util.build_location(ast, ns.filename)
79
+ ),
80
+ Util.build_public_method(
81
+ ns,
82
+ "#{mapping}_session",
83
+ location: Util.build_location(ast, ns.filename)
84
+ )
85
+ ]
86
+ end.flatten
87
+
88
+ pins += mapping_pins
89
+ Solargraph.logger.debug("[ARC][Devise] added #{pins.map(&:name)} to #{ns.path}") if pins.any?
90
+ pins
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,50 @@
1
+ module Solargraph
2
+ class ApiMap
3
+ # TODO: https://github.com/castwide/solargraph/pull/512
4
+ def get_complex_type_methods complex_type, context = '', internal = false
5
+ # This method does not qualify the complex type's namespace because
6
+ # it can cause conflicts between similar names, e.g., `Foo` vs.
7
+ # `Other::Foo`. It still takes a context argument to determine whether
8
+ # protected and private methods are visible.
9
+ return [] if complex_type.undefined? || complex_type.void?
10
+ result = Set.new
11
+ complex_type.each do |type|
12
+ if type.duck_type?
13
+ result.add Pin::DuckMethod.new(name: type.to_s[1..-1])
14
+ result.merge get_methods('Object')
15
+ else
16
+ unless type.nil? || type.name == 'void'
17
+ visibility = [:public]
18
+ if type.namespace == context || super_and_sub?(type.namespace, context)
19
+ visibility.push :protected
20
+ visibility.push :private if internal
21
+ end
22
+ result.merge get_methods(type.namespace, scope: type.scope, visibility: visibility)
23
+ end
24
+ end
25
+ end
26
+ result.to_a
27
+ end
28
+ end
29
+
30
+ class YardMap
31
+ # TODO: remove after https://github.com/castwide/solargraph/pull/509 is merged
32
+ def spec_for_require path
33
+ name = path.split('/').first
34
+ spec = Gem::Specification.find_by_name(name, @gemset[name])
35
+
36
+ # Avoid loading the spec again if it's going to be skipped anyway
37
+ #
38
+ return spec if @source_gems.include?(spec.name)
39
+ # Avoid loading the spec again if it's already the correct version
40
+ if @gemset[spec.name] && @gemset[spec.name] != spec.version
41
+ begin
42
+ return Gem::Specification.find_by_name(spec.name, "= #{@gemset[spec.name]}")
43
+ rescue Gem::LoadError
44
+ Solargraph.logger.warn "Unable to load #{spec.name} #{@gemset[spec.name]} specified by workspace, using #{spec.version} instead"
45
+ end
46
+ end
47
+ spec
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,90 @@
1
+ module Solargraph
2
+ module Arc
3
+ class RailsApi
4
+ def self.instance
5
+ @instance ||= self.new
6
+ end
7
+
8
+ def global yard_map
9
+ return [] if yard_map.required.empty?
10
+
11
+ ann = File.read(File.dirname(__FILE__) + "/annotations.rb")
12
+ source = Solargraph::Source.load_string(ann, "annotations.rb")
13
+ map = Solargraph::SourceMap.map(source)
14
+
15
+ Solargraph.logger.debug("[Arc][Rails] found #{map.pins.size} pins in annotations")
16
+
17
+ overrides = YAML.load_file(File.dirname(__FILE__) + "/types.yml").map do |meth, data|
18
+ if data["return"]
19
+ Util.method_return(meth, data["return"])
20
+ elsif data["yieldself"]
21
+ Solargraph::Pin::Reference::Override.from_comment(
22
+ meth,
23
+ "@yieldself [#{data['yieldself'].join(',')}]"
24
+ )
25
+ elsif data["yieldparam"]
26
+ Solargraph::Pin::Reference::Override.from_comment(
27
+ meth,
28
+ "@yieldparam [#{data['yieldparam'].join(',')}]"
29
+ )
30
+ end
31
+ end
32
+
33
+ ns = Solargraph::Pin::Namespace.new(
34
+ name: "ActionController::Base",
35
+ gates: ["ActionController::Base"]
36
+ )
37
+
38
+ definitions = [
39
+ Util.build_public_method(
40
+ ns,
41
+ "response",
42
+ types: ["ActionDispatch::Response"],
43
+ location: Util.dummy_location("whatever.rb")
44
+ ),
45
+ Util.build_public_method(
46
+ ns,
47
+ "request",
48
+ types: ["ActionDispatch::Request"],
49
+ location: Util.dummy_location("whatever.rb")
50
+ ),
51
+ Util.build_public_method(
52
+ ns,
53
+ "session",
54
+ types: ["ActionDispatch::Request::Session"],
55
+ location: Util.dummy_location("whatever.rb")
56
+ ),
57
+ Util.build_public_method(
58
+ ns,
59
+ "flash",
60
+ types: ["ActionDispatch::Flash::FlashHash"],
61
+ location: Util.dummy_location("whatever.rb")
62
+ )
63
+ ]
64
+
65
+ map.pins + definitions + overrides
66
+ end
67
+
68
+ def local(source_map, ns)
69
+ return [] unless source_map.filename.include?("db/migrate")
70
+ node = Walker.normalize_ast(source_map.source)
71
+
72
+ pins = [
73
+ Util.build_module_include(
74
+ ns,
75
+ "ActiveRecord::ConnectionAdapters::SchemaStatements",
76
+ Util.build_location(node, ns.filename)
77
+ ),
78
+ Util.build_module_extend(
79
+ ns,
80
+ "ActiveRecord::ConnectionAdapters::SchemaStatements",
81
+ Util.build_location(node, ns.filename)
82
+ )
83
+ ]
84
+
85
+ Solargraph.logger.debug("[ARC][RailsApi] added #{pins.map(&:name)} to #{ns.path}")
86
+ pins
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,60 @@
1
+ module Solargraph
2
+ module Arc
3
+ class Relation
4
+ def self.instance
5
+ @instance ||= self.new
6
+ end
7
+
8
+ def process(source_map, ns)
9
+ return [] unless source_map.filename.include?("app/models")
10
+
11
+ walker = Walker.from_source(source_map.source)
12
+ pins = []
13
+
14
+ walker.on :send, [nil, :belongs_to] do |ast|
15
+ pins << singular_association(ns, ast)
16
+ end
17
+
18
+ walker.on :send, [nil, :has_one] do |ast|
19
+ pins << singular_association(ns, ast)
20
+ end
21
+
22
+ walker.on :send, [nil, :has_many] do |ast|
23
+ pins << plural_association(ns, ast)
24
+ end
25
+
26
+ walker.on :send, [nil, :has_and_belongs_to_many] do |ast|
27
+ pins << plural_association(ns, ast)
28
+ end
29
+
30
+ walker.walk
31
+ Solargraph.logger.debug("[ARC][Relation] added #{pins.map(&:name)} to #{ns.path}") if pins.any?
32
+ pins
33
+ end
34
+
35
+ # TODO: handle custom class for relation
36
+ def plural_association(ns, ast)
37
+ relation_name = ast.children[2].children.first
38
+
39
+ Util.build_public_method(
40
+ ns,
41
+ relation_name.to_s,
42
+ types: ["ActiveRecord::Associations::CollectionProxy<#{relation_name.to_s.singularize.camelize}>"],
43
+ location: Util.build_location(ast, ns.filename)
44
+ )
45
+ end
46
+
47
+ # TODO: handle custom class for relation
48
+ def singular_association(ns, ast)
49
+ relation_name = ast.children[2].children.first
50
+
51
+ Util.build_public_method(
52
+ ns,
53
+ relation_name.to_s,
54
+ types: [relation_name.to_s.camelize],
55
+ location: Util.build_location(ast, ns.filename)
56
+ )
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,87 @@
1
+ module Solargraph
2
+ module Arc
3
+ class Schema
4
+ ColumnData = Struct.new(:type, :ast)
5
+
6
+ RUBY_TYPES = {
7
+ decimal: 'BigDecimal',
8
+ float: 'BigDecimal',
9
+ integer: 'Integer',
10
+ date: 'Date',
11
+ datetime: 'ActiveSupport::TimeWithZone',
12
+ string: 'String',
13
+ boolean: 'Boolean',
14
+ text: 'String',
15
+ jsonb: 'Hash',
16
+ bigint: 'Integer',
17
+ inet: 'IPAddr'
18
+ }
19
+
20
+ def self.instance
21
+ @instance ||= self.new
22
+ end
23
+
24
+ def initialize
25
+ @schema_present = File.exist?("db/schema.rb")
26
+ end
27
+
28
+ def process(source_map, ns)
29
+ return [] unless @schema_present
30
+ return [] unless source_map.filename.include?("app/models")
31
+
32
+ table_name = infer_table_name(ns)
33
+ table = schema[table_name]
34
+
35
+ return [] unless table
36
+
37
+ pins = table.map do |column, data|
38
+ Util.build_public_method(
39
+ ns,
40
+ column,
41
+ types: [RUBY_TYPES.fetch(data.type.to_sym)],
42
+ location: Util.build_location(data.ast, "db/schema.rb")
43
+ )
44
+ end
45
+
46
+ Solargraph.logger.debug("[ARC][Schema] added #{pins.map(&:name)} to #{ns.path}") if pins.any?
47
+ pins
48
+ end
49
+
50
+ private
51
+
52
+ def schema
53
+ @extracted_schema ||= begin
54
+ ast = NodeParser.parse(File.read("db/schema.rb"), "db/schema.rb")
55
+ extract_schema(ast)
56
+ end
57
+ end
58
+
59
+ # TODO: support custom table names, by parsing `self.table_name = ` invokations
60
+ # inside model
61
+ def infer_table_name(ns)
62
+ ns.name.underscore.pluralize
63
+ end
64
+
65
+ def extract_schema(ast)
66
+ schema = {}
67
+
68
+ walker = Walker.new(ast)
69
+ walker.on :block, [:send, nil, :create_table] do |ast, query|
70
+ table_name = ast.children.first.children[2].children.last
71
+ schema[table_name] = {}
72
+
73
+ query.on :send, [:lvar, :t] do |column_ast|
74
+ name = column_ast.children[2].children.last
75
+ type = column_ast.children[1]
76
+
77
+ next if type == :index
78
+ schema[table_name][name] = ColumnData.new(type, column_ast)
79
+ end
80
+ end
81
+
82
+ walker.walk
83
+ schema
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,42 @@
1
+ module Solargraph
2
+ module Arc
3
+ class Storage
4
+ def self.instance
5
+ @instance ||= self.new
6
+ end
7
+
8
+ def process(source_map, ns)
9
+ return [] unless source_map.filename.include?("app/models")
10
+
11
+ walker = Walker.from_source(source_map.source)
12
+ pins = []
13
+
14
+ walker.on :send, [nil, :has_one_attached] do |ast|
15
+ name = ast.children[2].children.first
16
+
17
+ pins << Util.build_public_method(
18
+ ns,
19
+ name.to_s,
20
+ types: ["ActiveStorage::Attached::One"],
21
+ location: Util.build_location(ast, ns.filename)
22
+ )
23
+ end
24
+
25
+ walker.on :send, [nil, :has_many_attached] do |ast|
26
+ name = ast.children[2].children.first
27
+
28
+ pins << Util.build_public_method(
29
+ ns,
30
+ name.to_s,
31
+ types: ["ActiveStorage::Attached::Many"],
32
+ location: Util.build_location(ast, ns.filename)
33
+ )
34
+ end
35
+
36
+ walker.walk
37
+ Solargraph.logger.debug("[ARC][Storage] added #{pins.map(&:name)} to #{ns.path}") if pins.any?
38
+ pins
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,22 @@
1
+ ActionController::Metal#params:
2
+ return: ["ActionController::Parameters"]
3
+ ActionController::Cookies#cookies:
4
+ return: ["ActionDispatch::Cookies::CookieJar"]
5
+ ActionDispatch::Flash::FlashHash#now:
6
+ return: ["ActionDispatch::Flash::FlashNow"]
7
+ ActiveRecord::QueryMethods#where:
8
+ return: ["self", "ActiveRecord::Relation", "ActiveRecord::QueryMethods::WhereChain"]
9
+ ActiveRecord::QueryMethods#not:
10
+ return: ["ActiveRecord::QueryMethods::WhereChain"]
11
+ ActiveRecord::FinderMethods#find_by:
12
+ return: ["self", "nil"]
13
+ Rails.application:
14
+ return: ["Rails::Application"]
15
+ ActionDispatch::Routing::RouteSet#draw:
16
+ yieldself: ["ActionDispatch::Routing::Mapper"]
17
+ ActiveRecord::ConnectionAdapters::SchemaStatements#create_table:
18
+ yieldparam: ["ActiveRecord::ConnectionAdapters::TableDefinition"]
19
+ ActiveRecord::ConnectionAdapters::SchemaStatements#create_join_table:
20
+ yieldparam: ["ActiveRecord::ConnectionAdapters::TableDefinition"]
21
+ ActiveRecord::ConnectionAdapters::SchemaStatements#change_table:
22
+ yieldparam: ["ActiveRecord::ConnectionAdapters::Table"]
@@ -0,0 +1,61 @@
1
+ module Solargraph
2
+ module Arc
3
+ module Util
4
+ def self.build_public_method(ns, name, types: nil, location: nil, attribute: false, scope: :instance)
5
+ opts = {
6
+ name: name,
7
+ location: location,
8
+ closure: ns,
9
+ scope: scope,
10
+ attribute: attribute
11
+ }
12
+
13
+ comments = []
14
+ comments << "@return [#{types.join(',')}]" if types
15
+
16
+ opts[:comments] = comments.join("\n")
17
+
18
+ Solargraph::Pin::Method.new(**opts)
19
+ end
20
+
21
+ def self.build_module_include(ns, module_name, location)
22
+ Solargraph::Pin::Reference::Include.new(
23
+ closure: ns,
24
+ name: module_name,
25
+ location: location
26
+ )
27
+ end
28
+
29
+ def self.build_module_extend(ns, module_name, location)
30
+ Solargraph::Pin::Reference::Extend.new(
31
+ closure: ns,
32
+ name: module_name,
33
+ location: location
34
+ )
35
+ end
36
+
37
+ def self.dummy_location(path)
38
+ Solargraph::Location.new(
39
+ path,
40
+ Solargraph::Range.from_to(0, 0, 0, 0)
41
+ )
42
+ end
43
+
44
+ def self.build_location(ast, path)
45
+ Solargraph::Location.new(
46
+ path,
47
+ Solargraph::Range.from_to(
48
+ ast.location.first_line,
49
+ 0,
50
+ ast.location.last_line,
51
+ ast.location.column
52
+ )
53
+ )
54
+ end
55
+
56
+ def self.method_return(path, type)
57
+ Solargraph::Pin::Reference::Override.method_return(path, type)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,5 @@
1
+ module Solargraph
2
+ module Arc
3
+ VERSION = "0.2.0"
4
+ end
5
+ end
@@ -0,0 +1,89 @@
1
+ module Solargraph
2
+ module Arc
3
+ class Walker
4
+ class Hook
5
+ attr_reader :args, :proc, :node_type
6
+ def initialize(node_type, args, &block)
7
+ @node_type = node_type
8
+ @args = args
9
+ @proc = Proc.new(&block)
10
+ end
11
+ end
12
+
13
+ # https://github.com/castwide/solargraph/issues/522
14
+ def self.normalize_ast(source)
15
+ ast = source.node
16
+
17
+ if ast.is_a?(::Parser::AST::Node)
18
+ ast
19
+ else
20
+ NodeParser.parse(source.code, source.filename)
21
+ end
22
+ end
23
+
24
+ def self.from_source(source)
25
+ self.new(self.normalize_ast(source))
26
+ end
27
+
28
+ def initialize(ast)
29
+ @ast = ast
30
+ @hooks = Hash.new([])
31
+ end
32
+
33
+ def on(node_type, args=[], &block)
34
+ @hooks[node_type] << Hook.new(node_type, args, &block)
35
+ end
36
+
37
+ def walk
38
+ if @ast.is_a?(Array)
39
+ @ast.each { |node| traverse(node) }
40
+ else
41
+ traverse(@ast)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def traverse(node)
48
+ return unless node.is_a?(::Parser::AST::Node)
49
+
50
+ @hooks[node.type].each do |hook|
51
+ try_match(node, hook)
52
+ end
53
+
54
+ node.children.each {|child| traverse(child) }
55
+ end
56
+
57
+ def try_match(node, hook)
58
+ return unless node.type == hook.node_type
59
+ return unless node.children
60
+
61
+ matched = hook.args.empty? || if node.children.first.is_a?(::Parser::AST::Node)
62
+ node.children.any? { |child| child.is_a?(::Parser::AST::Node) && match_children(hook.args[1..-1], child.children) }
63
+ else
64
+ match_children(hook.args, node.children)
65
+ end
66
+
67
+ if matched
68
+ if hook.proc.arity == 1
69
+ hook.proc.call(node)
70
+ elsif hook.proc.arity == 2
71
+ walker = Walker.new(node)
72
+ hook.proc.call(node, walker)
73
+ walker.walk
74
+ end
75
+ end
76
+ end
77
+
78
+ def match_children(args, children)
79
+ args.each_with_index.all? do |arg, i|
80
+ if children[i].is_a?(::Parser::AST::Node)
81
+ children[i].type == arg
82
+ else
83
+ children[i] == arg
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end