solargraph-rails 0.3.1 → 1.0.0.pre.1

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,100 @@
1
+ module Solargraph
2
+ module Rails
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
+ json: 'Hash',
17
+ bigint: 'Integer',
18
+ uuid: 'String',
19
+ inet: 'IPAddr'
20
+ }
21
+
22
+ def self.instance
23
+ @instance ||= self.new
24
+ end
25
+
26
+ def self.reset
27
+ @instance = nil
28
+ end
29
+
30
+ def initialize
31
+ @schema_present = File.exist?('db/schema.rb')
32
+ end
33
+
34
+ def process(source_map, ns)
35
+ return [] unless @schema_present
36
+ return [] unless source_map.filename.include?('app/models')
37
+
38
+ table_name = infer_table_name(ns)
39
+ table = schema[table_name]
40
+
41
+ return [] unless table
42
+
43
+ pins =
44
+ table.map do |column, data|
45
+ Util.build_public_method(
46
+ ns,
47
+ column,
48
+ types: [RUBY_TYPES.fetch(data.type.to_sym)],
49
+ location: Util.build_location(data.ast, 'db/schema.rb')
50
+ )
51
+ end
52
+
53
+ if pins.any?
54
+ Solargraph.logger.debug(
55
+ "[Rails][Schema] added #{pins.map(&:name)} to #{ns.path}"
56
+ )
57
+ end
58
+ pins
59
+ end
60
+
61
+ private
62
+
63
+ def schema
64
+ @extracted_schema ||=
65
+ begin
66
+ ast = NodeParser.parse(File.read('db/schema.rb'), 'db/schema.rb')
67
+ extract_schema(ast)
68
+ end
69
+ end
70
+
71
+ # TODO: support custom table names, by parsing `self.table_name = ` invokations
72
+ # inside model
73
+ def infer_table_name(ns)
74
+ ns.name.underscore.pluralize
75
+ end
76
+
77
+ def extract_schema(ast)
78
+ schema = {}
79
+
80
+ walker = Walker.new(ast)
81
+ walker.on :block, [:send, nil, :create_table] do |ast, query|
82
+ table_name = ast.children.first.children[2].children.last
83
+ schema[table_name] = {}
84
+
85
+ query.on :send, %i[lvar t] do |column_ast|
86
+ name = column_ast.children[2].children.last
87
+ type = column_ast.children[1]
88
+
89
+ next if type == :index
90
+ next if type == :check_constraint
91
+ schema[table_name][name] = ColumnData.new(type, column_ast)
92
+ end
93
+ end
94
+
95
+ walker.walk
96
+ schema
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,48 @@
1
+ module Solargraph
2
+ module Rails
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 <<
18
+ Util.build_public_method(
19
+ ns,
20
+ name.to_s,
21
+ types: ['ActiveStorage::Attached::One'],
22
+ location: Util.build_location(ast, ns.filename)
23
+ )
24
+ end
25
+
26
+ walker.on :send, [nil, :has_many_attached] do |ast|
27
+ name = ast.children[2].children.first
28
+
29
+ pins <<
30
+ Util.build_public_method(
31
+ ns,
32
+ name.to_s,
33
+ types: ['ActiveStorage::Attached::Many'],
34
+ location: Util.build_location(ast, ns.filename)
35
+ )
36
+ end
37
+
38
+ walker.walk
39
+ if pins.any?
40
+ Solargraph.logger.debug(
41
+ "[Rails][Storage] added #{pins.map(&:name)} to #{ns.path}"
42
+ )
43
+ end
44
+ pins
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,24 @@
1
+ ActionController::Metal#params:
2
+ return: ["ActionController::Parameters"]
3
+ ActiveRecord::FinderMethods#find:
4
+ return: ["self", "Array<self>"]
5
+ ActionController::Cookies#cookies:
6
+ return: ["ActionDispatch::Cookies::CookieJar"]
7
+ ActionDispatch::Flash::FlashHash#now:
8
+ return: ["ActionDispatch::Flash::FlashNow"]
9
+ ActiveRecord::QueryMethods#where:
10
+ return: ["self", "ActiveRecord::Relation", "ActiveRecord::QueryMethods::WhereChain"]
11
+ ActiveRecord::QueryMethods#not:
12
+ return: ["ActiveRecord::QueryMethods::WhereChain"]
13
+ ActiveRecord::FinderMethods#find_by:
14
+ return: ["self", "nil"]
15
+ Rails.application:
16
+ return: ["Rails::Application"]
17
+ ActionDispatch::Routing::RouteSet#draw:
18
+ yieldself: ["ActionDispatch::Routing::Mapper"]
19
+ ActiveRecord::ConnectionAdapters::SchemaStatements#create_table:
20
+ yieldparam: ["ActiveRecord::ConnectionAdapters::TableDefinition"]
21
+ ActiveRecord::ConnectionAdapters::SchemaStatements#create_join_table:
22
+ yieldparam: ["ActiveRecord::ConnectionAdapters::TableDefinition"]
23
+ ActiveRecord::ConnectionAdapters::SchemaStatements#change_table:
24
+ yieldparam: ["ActiveRecord::ConnectionAdapters::Table"]
@@ -0,0 +1,68 @@
1
+ module Solargraph
2
+ module Rails
3
+ module Util
4
+ def self.build_public_method(
5
+ ns,
6
+ name,
7
+ types: nil,
8
+ location: nil,
9
+ attribute: false,
10
+ scope: :instance
11
+ )
12
+ opts = {
13
+ name: name,
14
+ location: location,
15
+ closure: ns,
16
+ scope: scope,
17
+ attribute: attribute
18
+ }
19
+
20
+ comments = []
21
+ comments << "@return [#{types.join(',')}]" if types
22
+
23
+ opts[:comments] = comments.join("\n")
24
+
25
+ Solargraph::Pin::Method.new(**opts)
26
+ end
27
+
28
+ def self.build_module_include(ns, module_name, location)
29
+ Solargraph::Pin::Reference::Include.new(
30
+ closure: ns,
31
+ name: module_name,
32
+ location: location
33
+ )
34
+ end
35
+
36
+ def self.build_module_extend(ns, module_name, location)
37
+ Solargraph::Pin::Reference::Extend.new(
38
+ closure: ns,
39
+ name: module_name,
40
+ location: location
41
+ )
42
+ end
43
+
44
+ def self.dummy_location(path)
45
+ Solargraph::Location.new(
46
+ File.expand_path(path),
47
+ Solargraph::Range.from_to(0, 0, 0, 0)
48
+ )
49
+ end
50
+
51
+ def self.build_location(ast, path)
52
+ Solargraph::Location.new(
53
+ File.expand_path(path),
54
+ Solargraph::Range.from_to(
55
+ ast.location.first_line,
56
+ 0,
57
+ ast.location.last_line,
58
+ ast.location.column
59
+ )
60
+ )
61
+ end
62
+
63
+ def self.method_return(path, type)
64
+ Solargraph::Pin::Reference::Override.method_return(path, type)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,5 +1,5 @@
1
1
  module Solargraph
2
2
  module Rails
3
- VERSION = '0.3.1'
3
+ VERSION = '1.0.0.pre.1'
4
4
  end
5
5
  end
@@ -0,0 +1,89 @@
1
+ module Solargraph
2
+ module Rails
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_with_comments(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
+ attr_reader :ast, :comments
29
+ def initialize(ast, comments = {})
30
+ @comments = comments
31
+ @ast = ast
32
+ @hooks = Hash.new([])
33
+ end
34
+
35
+ def on(node_type, args = [], &block)
36
+ @hooks[node_type] << Hook.new(node_type, args, &block)
37
+ end
38
+
39
+ def walk
40
+ @ast.is_a?(Array) ? @ast.each { |node| traverse(node) } : traverse(@ast)
41
+ end
42
+
43
+ private
44
+
45
+ def traverse(node)
46
+ return unless node.is_a?(::Parser::AST::Node)
47
+
48
+ @hooks[node.type].each { |hook| try_match(node, hook) }
49
+
50
+ node.children.each { |child| traverse(child) }
51
+ end
52
+
53
+ def try_match(node, hook)
54
+ return unless node.type == hook.node_type
55
+ return unless node.children
56
+
57
+ matched =
58
+ hook.args.empty? || if node.children.first.is_a?(::Parser::AST::Node)
59
+ node.children.any? do |child|
60
+ child.is_a?(::Parser::AST::Node) &&
61
+ match_children(hook.args[1..-1], child.children)
62
+ end
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
@@ -1,36 +1,71 @@
1
- # frozen_string_literal: true
2
-
3
1
  require 'solargraph'
4
- require 'solargraph/rails/version'
5
- require_relative 'solargraph/rails/pin_creator'
6
- require_relative 'solargraph/rails/ruby_parser'
7
- require_relative 'solargraph/rails/files_loader'
8
- require_relative 'solargraph/rails/meta_source/association/belongs_to_matcher'
9
- require_relative 'solargraph/rails/meta_source/association/has_many_matcher'
10
- require_relative 'solargraph/rails/meta_source/association/has_one_matcher'
11
- require_relative 'solargraph/rails/meta_source/association/has_and_belongs_to_many_matcher'
2
+ require 'active_support/core_ext/string/inflections'
3
+
4
+ require_relative 'solargraph/rails/util.rb'
5
+ require_relative 'solargraph/rails/schema.rb'
6
+ require_relative 'solargraph/rails/annotate.rb'
7
+ require_relative 'solargraph/rails/autoload.rb'
8
+ require_relative 'solargraph/rails/model.rb'
9
+ require_relative 'solargraph/rails/devise.rb'
10
+ require_relative 'solargraph/rails/walker.rb'
11
+ require_relative 'solargraph/rails/rails_api.rb'
12
+ require_relative 'solargraph/rails/delegate.rb'
13
+ require_relative 'solargraph/rails/storage.rb'
14
+ require_relative 'solargraph/rails/debug.rb'
15
+ require_relative 'solargraph/rails/version.rb'
12
16
 
13
17
  module Solargraph
14
18
  module Rails
15
- class DynamicAttributes < Solargraph::Convention::Base
16
- def global yard_map
17
- Solargraph::Environ.new(pins: parse_models)
18
- end
19
+ class NodeParser
20
+ extend Solargraph::Parser::Legacy::ClassMethods
21
+ end
19
22
 
20
- private
23
+ class Convention < Solargraph::Convention::Base
24
+ def global(yard_map)
25
+ Solargraph::Environ.new(
26
+ pins: Solargraph::Rails::RailsApi.instance.global(yard_map)
27
+ )
28
+ rescue => error
29
+ Solargraph.logger.warn(
30
+ error.message + "\n" + error.backtrace.join("\n")
31
+ )
32
+ EMPTY_ENVIRON
33
+ end
21
34
 
22
- def parse_models
35
+ def local(source_map)
23
36
  pins = []
37
+ ds =
38
+ source_map.document_symbols.select do |n|
39
+ n.is_a?(Solargraph::Pin::Namespace)
40
+ end
41
+ ns = ds.first
24
42
 
25
- FilesLoader.new(
26
- Dir[File.join(Dir.pwd, 'app', 'models', '**', '*.rb')]
27
- ).each { |file, contents| pins.push *PinCreator.new(file, contents).create_pins }
43
+ return EMPTY_ENVIRON unless ns
28
44
 
29
- pins
45
+ pins += run_feature { Schema.instance.process(source_map, ns) }
46
+ pins += run_feature { Annotate.instance.process(source_map, ns) }
47
+ pins += run_feature { Model.instance.process(source_map, ns) }
48
+ pins += run_feature { Storage.instance.process(source_map, ns) }
49
+ pins += run_feature { Autoload.instance.process(source_map, ns, ds) }
50
+ pins += run_feature { Devise.instance.process(source_map, ns) }
51
+ pins += run_feature { Delegate.instance.process(source_map, ns) }
52
+ pins += run_feature { RailsApi.instance.local(source_map, ns) }
53
+
54
+ Solargraph::Environ.new(pins: pins)
55
+ end
56
+
57
+ private
58
+
59
+ def run_feature(&block)
60
+ yield
61
+ rescue => error
62
+ Solargraph.logger.warn(
63
+ error.message + "\n" + error.backtrace.join("\n")
64
+ )
65
+ []
30
66
  end
31
67
  end
32
68
  end
33
69
  end
34
70
 
35
-
36
- Solargraph::Convention.register Solargraph::Rails::DynamicAttributes
71
+ Solargraph::Convention.register(Solargraph::Rails::Convention)
@@ -0,0 +1,134 @@
1
+ require File.join(Dir.pwd, ARGV.first, 'config/environment')
2
+
3
+ class Model < ActiveRecord::Base
4
+ end
5
+
6
+ def own_instance_methods(klass, test = klass.new)
7
+ (instance_methods(klass, test) - Object.methods).select do |m|
8
+ m.source_location && m.source_location.first.include?('gem')
9
+ end
10
+ end
11
+
12
+ def own_class_methods(klass)
13
+ (class_methods(klass) - Object.methods).select do |m|
14
+ m.source_location && m.source_location.first.include?('gem')
15
+ end
16
+ end
17
+
18
+ def instance_methods(klass, test = klass.new)
19
+ klass
20
+ .instance_methods(true)
21
+ .sort
22
+ .reject { |m| m.to_s.start_with?('_') || (test && !test.respond_to?(m)) }
23
+ .map { |m| klass.instance_method(m) }
24
+ end
25
+
26
+ def class_methods(klass)
27
+ klass
28
+ .methods(true)
29
+ .sort
30
+ .reject { |m| m.to_s.start_with?('_') || !klass.respond_to?(m) }
31
+ .map { |m| klass.method(m) }
32
+ end
33
+
34
+ def build_report(klass, test: klass.new)
35
+ result = {}
36
+ distribution = {}
37
+
38
+ own_class_methods(klass).each do |meth|
39
+ distribution[meth.source_location.first] ||= []
40
+ distribution[meth.source_location.first] << ".#{meth.name}"
41
+
42
+ result["#{klass.to_s}.#{meth.name}"] = { types: ['undefined'], skip: false }
43
+ end
44
+
45
+ own_instance_methods(klass, test).each do |meth|
46
+ distribution[meth.source_location.first] ||= []
47
+ distribution[meth.source_location.first] << "##{meth.name}"
48
+
49
+ result["#{klass.to_s}##{meth.name}"] = { types: ['undefined'], skip: false }
50
+ end
51
+
52
+ pp distribution
53
+ result
54
+ end
55
+
56
+ def core_ext_report(klass, test = klass.new)
57
+ result = {}
58
+ distribution = {}
59
+
60
+ class_methods(klass)
61
+ .select(&:source_location)
62
+ .select do |meth|
63
+ loc = meth.source_location.first
64
+ loc.include?('activesupport') && loc.include?('core_ext')
65
+ end
66
+ .each do |meth|
67
+ distribution[meth.source_location.first] ||= []
68
+ distribution[meth.source_location.first] << ".#{meth.name}"
69
+
70
+ result["#{klass.to_s}.#{meth.name}"] = {
71
+ types: ['undefined'],
72
+ skip: false
73
+ }
74
+ end
75
+
76
+ instance_methods(klass, test)
77
+ .select(&:source_location)
78
+ .select do |meth|
79
+ loc = meth.source_location.first
80
+ loc.include?('activesupport') && loc.include?('core_ext')
81
+ end
82
+ .each do |meth|
83
+ distribution[meth.source_location.first] ||= []
84
+ distribution[meth.source_location.first] << "##{meth.name}"
85
+
86
+ result["#{klass.to_s}##{meth.name}"] = {
87
+ types: ['undefined'],
88
+ skip: false
89
+ }
90
+ end
91
+
92
+ result
93
+ end
94
+
95
+ report = build_report(ActiveRecord::Base, test: Model.new)
96
+ File.write('activerecord.yml', report.deep_stringify_keys.to_yaml)
97
+
98
+ report = build_report(ActionController::Base)
99
+ File.write('actioncontroller.yml', report.deep_stringify_keys.to_yaml)
100
+
101
+ report = build_report(ActiveJob::Base)
102
+ File.write('activejob.yml', report.deep_stringify_keys.to_yaml)
103
+
104
+ Rails.application.routes.draw do
105
+ report = build_report(self.class, test: false)
106
+ File.write('routes.yml', report.deep_stringify_keys.to_yaml)
107
+ end
108
+
109
+ # [Array, String, Time, Date, Class, DateTime, File, Hash, Integer, Kernel].each do |klass|
110
+ # test = case klass
111
+ # when Time
112
+ # Time.now
113
+ # when Date
114
+ # Date.today
115
+ # when File
116
+ # false
117
+ # else
118
+ # klass.new rescue false
119
+ # end
120
+ #
121
+ # report = core_ext_report(klass, test=test)
122
+ # File.write("#{klass.to_s}.yml", report.deep_stringify_keys.to_yaml)
123
+ # end
124
+
125
+ # binding.pry
126
+
127
+ # File.write("activerecord.yml", result.deep_stringify_keys.to_yaml)
128
+
129
+ # report = build_report(
130
+ # ActionController::Base,
131
+ # own_class_methods(ActionController::Base),
132
+ # own_instance_methods(ActionController::Base)
133
+ # )
134
+ # File.write("actioncontroller.yml", report.deep_stringify_keys.to_yaml)
@@ -1,30 +1,32 @@
1
-
2
- lib = File.expand_path("../lib", __FILE__)
1
+ lib = File.expand_path('../lib', __FILE__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "solargraph/rails/version"
3
+ require 'solargraph/rails/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "solargraph-rails"
8
- spec.version = Solargraph::Rails::VERSION
9
- spec.authors = ["Fritz Meissner"]
10
- spec.email = ["fritz.meissner@gmail.com"]
6
+ spec.name = 'solargraph-rails'
7
+ spec.version = Solargraph::Rails::VERSION
8
+ spec.authors = ['Fritz Meissner']
9
+ spec.email = ['fritz.meissner@gmail.com']
11
10
 
12
- spec.summary = %q{Solargraph plugin that adds Rails-specific code through a Convention}
13
- spec.description = %q{Add reflection on ActiveModel dynamic attributes that will be created at runtime}
14
- spec.homepage = 'https://github.com/iftheshoefritz/solargraph-rails'
15
- spec.license = "MIT"
11
+ spec.summary =
12
+ 'Solargraph plugin that adds Rails-specific code through a Convention'
13
+ spec.description =
14
+ 'Add reflection on ActiveModel dynamic attributes that will be created at runtime'
15
+ spec.homepage = 'https://github.com/iftheshoefritz/solargraph-rails'
16
+ spec.license = 'MIT'
16
17
 
17
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
- f.match(%r{^(test|spec|features)/})
19
- end
20
- spec.bindir = "exe"
21
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
- spec.require_paths = ["lib"]
18
+ spec.files =
19
+ `git ls-files -z`.split("\x0").reject do |f|
20
+ f.match(%r{^(test|spec|features)/})
21
+ end
22
+ spec.bindir = 'exe'
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ['lib']
23
25
 
24
- spec.add_development_dependency "bundler", "~> 2.2.10"
25
- spec.add_development_dependency "rake", "~> 12.3.3"
26
- spec.add_development_dependency "rspec", "~> 3.0"
26
+ spec.add_development_dependency 'bundler', '~> 2.3'
27
+ spec.add_development_dependency 'rake', '~> 12.3.3'
28
+ spec.add_development_dependency 'rspec', '~> 3.0'
27
29
 
28
- spec.add_runtime_dependency "solargraph", ">= 0.41.1"
29
- spec.add_runtime_dependency "activesupport"
30
+ spec.add_runtime_dependency 'solargraph', '~> 0.44.2'
31
+ spec.add_runtime_dependency 'activesupport'
30
32
  end