solargraph-rails 0.3.1 → 1.0.0.pre.1

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