solargraph-arc 0.2.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.
@@ -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