Class: Pinspec::Analyzer::TargetParser

Inherits:
Object
  • Object
show all
Includes:
Source
Defined in:
lib/pinspec/analyzer/target_parser.rb

Defined Under Namespace

Classes: Delegation, Descriptor, MethodDef, Scope

Constant Summary collapse

MODEL_BASES =
["ApplicationRecord", "ActiveRecord::Base"].freeze
INERT_BASES =
%w[Object BasicObject].freeze
CLOCK_CALLS =
{
  "Time"     => %i[now new],
  "Date"     => %i[today],
  "DateTime" => %i[now]
}.freeze
VISIBILITIES =
%i[public private protected].freeze
HASH_SHAPED =

Names that are a bag of attributes rather than a model. create_params, options, article_attributes.

/\A(?:\w+_)?(?:params|options|attributes|attrs|opts)\z/
SCALARISH_TYPES =

These names are not models - but saying only that leaves the corpus with no value at all, and a required parameter then receives an invented nil. The target raises on it and pinspec pins that error as the application's behaviour, which is the one thing it promises never to do. So each name maps to a type the corpus can actually build.

{
  "String" => %w[
    name email phone title body text message description reason code key
    path url slug token label format mode scope type kind status state
    value data
  ],
  "Integer" => %w[count index number num limit offset size length quantity qty position],
  "Float" => %w[amount total subtotal price cost percent rate ratio sum]
}.freeze
SCALARISH_NAMES =
SCALARISH_TYPES.values.flatten.freeze
DI_PATTERNS =
[
  /\ARails\.application\.config\b/,
  /\ARails\.configuration\b/,
  /Container\b.*\.resolve\b/,
  /\AContainer\./
].freeze
PRUNE_AT =
[
  Prism::DefNode,
  Prism::ClassNode,
  Prism::ModuleNode,
  Prism::SingletonClassNode
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Source

read

Constructor Details

#initialize(file_path, method_name, app_root: nil) ⇒ TargetParser

Returns a new instance of TargetParser.



116
117
118
119
120
# File 'lib/pinspec/analyzer/target_parser.rb', line 116

def initialize(file_path, method_name, app_root: nil)
  @app_root   = app_root
  @file_path  = file_path
  @descriptor = parse_descriptor(method_name)
end

Class Method Details

.application_scope_cache(root) ⇒ Object



104
105
106
107
# File 'lib/pinspec/analyzer/target_parser.rb', line 104

def application_scope_cache(root)
  @application_scopes ||= {}
  @application_scopes[root] ||= {}
end

.application_sources_for(root) ⇒ Object

Keyed by application root, because a batch run builds one parser per file and re-globbing the application for each would be quadratic.



96
97
98
99
100
101
102
# File 'lib/pinspec/analyzer/target_parser.rb', line 96

def application_sources_for(root)
  @application_sources ||= {}
  @application_sources[root] ||= begin
    dirs = %w[app lib].map { |d| File.join(root, d) }.select { |d| File.directory?(d) }
    dirs.flat_map { |dir| Dir.glob(File.join(dir, "**", "*.rb")) }.sort
  end
end

.parse(file_path, method_name) ⇒ Object



72
73
74
# File 'lib/pinspec/analyzer/target_parser.rb', line 72

def parse(file_path, method_name)
  new(file_path, method_name).parse
end

.reset_application_cache!Object

Tests and long-lived processes need a way back to a clean slate.



110
111
112
113
# File 'lib/pinspec/analyzer/target_parser.rb', line 110

def reset_application_cache!
  @application_sources = {}
  @application_scopes = {}
end

.split_target(target) ⇒ Object

Returns [file, method] with method nil when the target named none. It is the caller's job to discover one, because that needs the file's contents and the surrounding directory's convention.



79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/pinspec/analyzer/target_parser.rb', line 79

def split_target(target)
  text = target.to_s
  return text.split("#", 2) if text.include?("#")

  unless text.end_with?(".rb")
    raise ArgumentError,
          "target must be a Ruby file, optionally with #METHOD " \
          "(got #{target.inspect}); e.g. app/services/invoice_calculator.rb#call"
  end

  [text, nil]
end

Instance Method Details

#parseObject



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/pinspec/analyzer/target_parser.rb', line 122

def parse
  read_source
  build_index

  mdef  = select_definition
  scope = @scopes[mdef.scope_name]

  unless scope
    raise TargetNotFound,
          "`#{@descriptor.method_name}` is defined at the top level of " \
          "#{@file_path}, not inside a class or module; pinspec has no " \
          "receiver to build."
  end

  guard_block!(mdef, scope)

  construction_kind, initializer_params = resolve_construction(scope, mdef)
  init_node = find_initialize(scope.name)

  scan = [mdef.node, init_node].compact

  TargetProfile.new(
    file_path:            @file_path,
    class_name:           scope.name,
    method_name:          mdef.name,
    params:               params_of(mdef.node),
    initializer_params:   initializer_params,
    construction_kind:    construction_kind,
    visibility:           mdef.visibility,
    takes_block:          false,
    source_range:         [mdef.node.location.start_line, mdef.node.location.end_line],
    referenced_constants: referenced_constants(scan),
    clock_sites:          clock_sites(scan),
    construction_source:  @construction_source || :own
  )
end