Class: Bullematic::Detection

Inherits:
Object
  • Object
show all
Defined in:
lib/bullematic/detection.rb,
sig/generated/bullematic/detection.rbs

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(type:, base_class:, associations:, call_stack:, context_id: nil, source_digest: nil) ⇒ Detection

Returns a new instance of Detection.

RBS:

  • type: Symbol

  • base_class: untyped

  • associations: untyped

  • call_stack: Array[String]

  • context_id: untyped

  • source_digest: String?

  • return: void

Parameters:

  • type: (Symbol)
  • base_class: (Object)
  • associations: (Object)
  • call_stack: (Array[String])
  • context_id: (Object) (defaults to: nil)
  • source_digest: (String, nil) (defaults to: nil)


39
40
41
42
43
44
45
46
47
# File 'lib/bullematic/detection.rb', line 39

def initialize(type:, base_class:, associations:, call_stack:, context_id: nil, source_digest: nil)
  @type = type
  @base_class = base_class
  @associations = normalize_associations(associations)
  @call_stack = call_stack
  @context_id = context_id
  parse_source_location
  @source_digest = source_digest || current_source_digest
end

Instance Attribute Details

#associationsObject (readonly)

RBS:

  • attr_reader type: Symbol
    attr_reader base_class: untyped
    attr_reader associations: Array[Symbol]
    attr_reader call_stack: Array[String]
    attr_reader source_file: String?
    attr_reader line_number: Integer?
    attr_reader method_name: String?
    attr_reader context_id: untyped
    attr_reader source_digest: String?

Returns:

  • (Object)


29
30
31
# File 'lib/bullematic/detection.rb', line 29

def associations
  @associations
end

#base_classObject (readonly)

RBS:

  • attr_reader type: Symbol
    attr_reader base_class: untyped
    attr_reader associations: Array[Symbol]
    attr_reader call_stack: Array[String]
    attr_reader source_file: String?
    attr_reader line_number: Integer?
    attr_reader method_name: String?
    attr_reader context_id: untyped
    attr_reader source_digest: String?

Returns:

  • (Object)


29
30
31
# File 'lib/bullematic/detection.rb', line 29

def base_class
  @base_class
end

#call_stackObject (readonly)

RBS:

  • attr_reader type: Symbol
    attr_reader base_class: untyped
    attr_reader associations: Array[Symbol]
    attr_reader call_stack: Array[String]
    attr_reader source_file: String?
    attr_reader line_number: Integer?
    attr_reader method_name: String?
    attr_reader context_id: untyped
    attr_reader source_digest: String?

Returns:

  • (Object)


29
30
31
# File 'lib/bullematic/detection.rb', line 29

def call_stack
  @call_stack
end

#context_idObject (readonly)

RBS:

  • attr_reader type: Symbol
    attr_reader base_class: untyped
    attr_reader associations: Array[Symbol]
    attr_reader call_stack: Array[String]
    attr_reader source_file: String?
    attr_reader line_number: Integer?
    attr_reader method_name: String?
    attr_reader context_id: untyped
    attr_reader source_digest: String?

Returns:

  • (Object)


29
30
31
# File 'lib/bullematic/detection.rb', line 29

def context_id
  @context_id
end

#line_numberObject (readonly)

RBS:

  • attr_reader type: Symbol
    attr_reader base_class: untyped
    attr_reader associations: Array[Symbol]
    attr_reader call_stack: Array[String]
    attr_reader source_file: String?
    attr_reader line_number: Integer?
    attr_reader method_name: String?
    attr_reader context_id: untyped
    attr_reader source_digest: String?

Returns:

  • (Object)


29
30
31
# File 'lib/bullematic/detection.rb', line 29

def line_number
  @line_number
end

#method_nameObject (readonly)

RBS:

  • attr_reader type: Symbol
    attr_reader base_class: untyped
    attr_reader associations: Array[Symbol]
    attr_reader call_stack: Array[String]
    attr_reader source_file: String?
    attr_reader line_number: Integer?
    attr_reader method_name: String?
    attr_reader context_id: untyped
    attr_reader source_digest: String?

Returns:

  • (Object)


29
30
31
# File 'lib/bullematic/detection.rb', line 29

def method_name
  @method_name
end

#source_digestObject (readonly)

RBS:

  • attr_reader type: Symbol
    attr_reader base_class: untyped
    attr_reader associations: Array[Symbol]
    attr_reader call_stack: Array[String]
    attr_reader source_file: String?
    attr_reader line_number: Integer?
    attr_reader method_name: String?
    attr_reader context_id: untyped
    attr_reader source_digest: String?

Returns:

  • (Object)


29
30
31
# File 'lib/bullematic/detection.rb', line 29

def source_digest
  @source_digest
end

#source_fileObject (readonly)

RBS:

  • attr_reader type: Symbol
    attr_reader base_class: untyped
    attr_reader associations: Array[Symbol]
    attr_reader call_stack: Array[String]
    attr_reader source_file: String?
    attr_reader line_number: Integer?
    attr_reader method_name: String?
    attr_reader context_id: untyped
    attr_reader source_digest: String?

Returns:

  • (Object)


29
30
31
# File 'lib/bullematic/detection.rb', line 29

def source_file
  @source_file
end

#typeObject (readonly)

RBS:

  • attr_reader type: Symbol
    attr_reader base_class: untyped
    attr_reader associations: Array[Symbol]
    attr_reader call_stack: Array[String]
    attr_reader source_file: String?
    attr_reader line_number: Integer?
    attr_reader method_name: String?
    attr_reader context_id: untyped
    attr_reader source_digest: String?

Returns:

  • (Object)


29
30
31
# File 'lib/bullematic/detection.rb', line 29

def type
  @type
end

Class Method Details

.from_h(data) ⇒ Detection

RBS:

  • data: Hash[String, untyped]

  • return: Detection

Parameters:

  • data (Hash[String, untyped])

Returns:



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/bullematic/detection.rb', line 51

def self.from_h(data)
  type = data.fetch("type").to_s.to_sym
  associations = data.fetch("associations")
  call_stack = data.fetch("call_stack")
  raise ArgumentError, "invalid evidence type" unless %i[n_plus_one unused_eager_loading].include?(type)
  raise ArgumentError, "invalid evidence associations" unless associations.is_a?(Array)
  raise ArgumentError, "invalid evidence call stack" unless call_stack.is_a?(Array)
  source_digest = data["source_digest"]
  raise ArgumentError, "invalid evidence source digest" unless source_digest.is_a?(String) && source_digest.match?(/\A[0-9a-f]{64}\z/)

  new(
    type: type,
    base_class: data.fetch("base_class"),
    associations: associations,
    call_stack: call_stack,
    context_id: data["context_id"],
    source_digest: source_digest
  )
end

Instance Method Details

#absolute_path(path) ⇒ Pathname

RBS:

  • path: String

  • return: Pathname

Parameters:

  • path (String)

Returns:

  • (Pathname)


188
189
190
191
192
193
194
195
# File 'lib/bullematic/detection.rb', line 188

def absolute_path(path)
  pathname = Pathname.new(path)
  return pathname if path.match?(%r{\A[A-Za-z]:[\\/]})
  return pathname.expand_path if pathname.absolute?

  root = defined?(Rails) && Rails.respond_to?(:root) && Rails.root ? Rails.root : Dir.pwd
  Pathname.new(root).join(pathname).expand_path
end

#current_source_digestString?

RBS:

  • return: String?

Returns:

  • (String, nil)


198
199
200
201
202
203
204
# File 'lib/bullematic/detection.rb', line 198

def current_source_digest
  return unless source_file && File.file?(source_file)

  Digest::SHA256.hexdigest(File.binread(source_file))
rescue Errno::ENOENT, Errno::EACCES
  nil
end

#expand_path(path) ⇒ Pathname

RBS:

  • path: String

  • return: Pathname

Parameters:

  • path (String)

Returns:

  • (Pathname)


175
176
177
178
179
180
181
182
183
184
# File 'lib/bullematic/detection.rb', line 175

def expand_path(path)
  expanded = absolute_path(path)
  return expanded unless expanded.exist?

  begin
    expanded.realpath
  rescue Errno::ENOENT, Errno::EACCES
    expanded
  end
end

#fingerprintArray[untyped]

: () -> Array

Returns:

  • (Array[untyped])


98
99
100
# File 'lib/bullematic/detection.rb', line 98

def fingerprint
  [type, context_id, source_file, line_number, model_class_name, associations.sort]
end

#fixable?Boolean

: () -> bool

Returns:

  • (Boolean)


89
90
91
92
93
94
95
# File 'lib/bullematic/detection.rb', line 89

def fixable?
  !source_file.nil? &&
    !source_file.empty? &&
    !associations.empty? &&
    target_path? &&
    !skip_path?
end

#model_class_nameString

: () -> String

Returns:

  • (String)


84
85
86
# File 'lib/bullematic/detection.rb', line 84

def model_class_name
  base_class.to_s
end

#normalize_associations(assocs) ⇒ Array[Symbol]

RBS:

  • assocs: untyped

  • return: Array[Symbol]

Parameters:

  • assocs (Object)

Returns:

  • (Array[Symbol])


106
107
108
109
110
111
# File 'lib/bullematic/detection.rb', line 106

def normalize_associations(assocs)
  values = Array(assocs) #: Array[untyped]
  values.filter_map do |association|
    association.to_sym if association.is_a?(String) || association.is_a?(Symbol)
  end.uniq
end

#parse_source_locationvoid

This method returns an undefined value.

: () -> void



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/bullematic/detection.rb', line 114

def parse_source_location
  config = Bullematic.configuration
  return unless config

  target_paths = config.target_paths

  @call_stack.each do |raw_frame|
    frame = raw_frame.to_s

    match = frame.match(/\A(.+):(\d+)(?::in [`'](.+)')?\z/)
    next unless match

    filepath = match[1]
    line = match[2].to_i
    method = match[3]

    next unless filepath && path_in?(filepath, target_paths)

    @source_file = absolute_path(filepath).to_s
    @line_number = line
    @method_name = method
    break
  end
end

#path_in?(filepath, paths) ⇒ Boolean

RBS:

  • filepath: String

  • paths: Array[String]

  • return: bool

Parameters:

  • filepath (String)
  • paths (Array[String])

Returns:

  • (Boolean)


162
163
164
165
166
167
168
169
170
171
# File 'lib/bullematic/detection.rb', line 162

def path_in?(filepath, paths)
  source = expand_path(filepath)

  paths.any? do |path|
    next false unless path.is_a?(String) && !path.empty?

    target = expand_path(path)
    source == target || source.to_s.start_with?("#{target}#{File::SEPARATOR}")
  end
end

#skip_path?Boolean

: () -> bool

Returns:

  • (Boolean)


150
151
152
153
154
155
156
157
# File 'lib/bullematic/detection.rb', line 150

def skip_path?
  return false if source_file.nil?

  config = Bullematic.configuration
  return false unless config

  path_in?(source_file, config.skip_paths)
end

#target_path?Boolean

: () -> bool

Returns:

  • (Boolean)


140
141
142
143
144
145
146
147
# File 'lib/bullematic/detection.rb', line 140

def target_path?
  return false if source_file.nil?

  config = Bullematic.configuration
  return false unless config

  path_in?(source_file, config.target_paths)
end

#to_hHash[String, untyped]

RBS:

  • return: Hash[String, untyped]

Returns:

  • (Hash[String, untyped])


72
73
74
75
76
77
78
79
80
81
# File 'lib/bullematic/detection.rb', line 72

def to_h
  {
    "type" => type.to_s,
    "base_class" => model_class_name,
    "associations" => associations.map(&:to_s),
    "call_stack" => call_stack.map(&:to_s),
    "context_id" => context_id,
    "source_digest" => source_digest
  }
end