Class: QueryPackwerk::Violation

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/query_packwerk/violation.rb

Overview

Represents a single Packwerk violation with extended inspection capabilities. Provides methods to analyze violation details including source location, contextual information, and code patterns. Facilitates both detailed and anonymized views of dependency violations between packages.

Constant Summary collapse

RUBY_FILE =

This does not play nicely with ERB files which may have violations

T.let(/\.(rb|rake)\z/, Regexp)
ALL_CAPS =
T.let(/\A[A-Z_]+\z/, Regexp)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(original_violation:, consuming_pack:, file_cache: QueryPackwerk::FileCache.new) ⇒ Violation

Returns a new instance of Violation.



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/query_packwerk/violation.rb', line 29

def initialize(original_violation:, consuming_pack:, file_cache: QueryPackwerk::FileCache.new)
  @original_violation = original_violation

  @producing_pack = T.let(
    QueryPackwerk::Package.new(original_package: T.must(ParsePackwerk.find(original_violation.to_package_name))),
    QueryPackwerk::Package
  )

  @consuming_pack = T.let(
    QueryPackwerk::Package.new(original_package: consuming_pack),
    QueryPackwerk::Package
  )

  @file_cache = T.let(file_cache, QueryPackwerk::FileCache)
  @cache_loaded = T.let(false, T::Boolean)
end

Instance Attribute Details

#consuming_packObject (readonly)

Returns the value of attribute consuming_pack.



20
21
22
# File 'lib/query_packwerk/violation.rb', line 20

def consuming_pack
  @consuming_pack
end

#producing_packObject (readonly)

Returns the value of attribute producing_pack.



17
18
19
# File 'lib/query_packwerk/violation.rb', line 17

def producing_pack
  @producing_pack
end

Instance Method Details

#anonymous_source_countsObject



194
195
196
# File 'lib/query_packwerk/violation.rb', line 194

def anonymous_source_counts
  anonymous_sources.tally
end

#anonymous_sourcesObject



139
140
141
142
143
144
145
146
# File 'lib/query_packwerk/violation.rb', line 139

def anonymous_sources
  load_cache!

  files.flat_map do |file_name|
    @file_cache
      .get_full_anonymous_sources(file_name: file_name, class_name: class_name)
  end
end

#anonymous_sources_with_locationsObject



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/query_packwerk/violation.rb', line 150

def anonymous_sources_with_locations
  load_cache!

  file_sources = files.flat_map do |file_name|
    @file_cache.get_full_sources(file_name: file_name, class_name: class_name).map do |s|
      ["#{file_name}:#{s.loc.line}", @file_cache.anonymize_arguments(s.source)]
    end
  end

  anonymous_source_groups = Hash.new { |h, source| h[source] = [] }

  file_sources.each_with_object(anonymous_source_groups) do |(location, source), groups|
    groups[source] << location
  end
end

#class_nameObject



78
79
80
# File 'lib/query_packwerk/violation.rb', line 78

def class_name
  @original_violation.class_name
end

#countObject



201
202
203
204
205
206
207
208
# File 'lib/query_packwerk/violation.rb', line 201

def count
  files.sum do |file_name|
    @file_cache.get_all_const_occurrences(
      file_name: file_name,
      class_name: class_name
    ).size
  end
end

#deconstruct_keys(keys) ⇒ Object



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/query_packwerk/violation.rb', line 248

def deconstruct_keys(keys)
  all_values = {
    constant_name: class_name,
    pack_name: to_package_name,

    # Type related properties, including convenience boolean handlers
    type: type,
    privacy: type == 'privacy',
    dependency: type == 'dependency',

    # Reaching into which pack produced the violated constant, and
    # which consumes the violated constant.
    consuming_pack: consuming_pack.name,
    producing_pack: producing_pack.name,

    # Same, except for owners
    producing_owner: producing_pack.owner,
    consuming_owner: consuming_pack.owner,

    # So why is this "owner" implying producer? Because the
    # owner field of the violation is producer-oriented.
    owner: producing_pack.owner,
    owned: producing_pack.owner.nil?,

    **runtime_keys(keys)
  }

  # all_values[:is_active_record] = active_record? if !keys || keys.include?(:is_active_record)
  # all_values[:is_constant] = active_record? if !keys || keys.include?(:is_constant)

  keys.nil? ? all_values : all_values.slice(*T.unsafe(keys))
end

#file_countObject



61
62
63
# File 'lib/query_packwerk/violation.rb', line 61

def file_count
  files.size
end

#filesObject



83
84
85
# File 'lib/query_packwerk/violation.rb', line 83

def files
  @original_violation.files
end

#includes_files?(*globs) ⇒ Boolean

Returns:

  • (Boolean)


93
94
95
96
97
98
99
# File 'lib/query_packwerk/violation.rb', line 93

def includes_files?(*globs)
  globs.any? do |glob|
    files.any? do |file_name|
      glob.is_a?(Regexp) ? glob.match?(file_name) : File.fnmatch?(glob, file_name)
    end
  end
end

#inspectObject



282
283
284
# File 'lib/query_packwerk/violation.rb', line 282

def inspect
  "#<#{self.class.name} #{consuming_pack.name} -> #{class_name} (#{type})>"
end

#load_cache!(headers: false) ⇒ Object



47
48
49
50
51
52
# File 'lib/query_packwerk/violation.rb', line 47

def load_cache!(headers: false)
  return true if @cache_loaded

  @file_cache.load!(*T.unsafe(files), headers: headers)
  @cache_loaded = true
end

#runtime_keys(keys) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/query_packwerk/violation.rb', line 213

def runtime_keys(keys)
  return {} unless defined?(Rails)

  runtime_values = {}

  return { is_active_record: false, is_constant: false } unless Kernel.const_defined?(class_name)

  if keys.nil? || keys.include?(:is_active_record)
    constant = Kernel.const_get(class_name) # rubocop:disable Sorbet/ConstantsFromStrings

    value = @file_cache.set(
      :is_active_record,
      key: class_name,
      value: constant.is_a?(Class) && constant < ApplicationRecord
    )

    runtime_values[:is_active_record] = value
  end

  if keys.nil? || keys.include?(:is_constant)
    value = @file_cache.set(
      :is_constant,
      key: class_name,
      value: class_name.split('::').last&.match?(ALL_CAPS)
    )

    runtime_values[:is_constant] = value
  end

  runtime_values
end

#set_cache!(cache) ⇒ Object



55
56
57
58
# File 'lib/query_packwerk/violation.rb', line 55

def set_cache!(cache)
  @cache_loaded = false
  @file_cache = cache
end

#source_countsObject



125
126
127
128
129
130
131
132
133
134
135
# File 'lib/query_packwerk/violation.rb', line 125

def source_counts
  load_cache!

  sources = files.flat_map do |file_name|
    @file_cache
      .get_full_sources(file_name: file_name, class_name: class_name)
      .map(&:source)
  end

  sources.tally
end

#sourcesObject



103
104
105
106
107
108
109
# File 'lib/query_packwerk/violation.rb', line 103

def sources
  load_cache!

  files.flat_map do |file_name|
    @file_cache.get_full_sources(file_name: file_name, class_name: class_name)
  end
end

#sources_with_contexts(start_offset: 3, end_offset: 3) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/query_packwerk/violation.rb', line 167

def sources_with_contexts(start_offset: 3, end_offset: 3)
  load_cache!

  file_sources = files.flat_map do |file_name|
    @file_cache.get_full_sources(file_name: file_name, class_name: class_name).map do |s|
      line_number = s.loc.line
      start_pos = line_number - start_offset
      end_pos = line_number + end_offset

      location = "#{file_name}:#{s.loc.line} (L#{start_pos}..#{end_pos})"
      context = @file_cache.get_file(file_name).lines.slice(start_pos..end_pos)
      full_context = unindent((context || ['']).join)

      [@file_cache.anonymize_arguments(s.source), "> #{location}\n\n#{full_context}"]
    end
  end

  anonymous_source_groups = Hash.new { |h, source| h[source] = [] }

  file_sources.each_with_object(anonymous_source_groups) do |(anonymous_source, full_source), groups|
    groups[anonymous_source] << full_source
  end
end

#sources_with_locationsObject



113
114
115
116
117
118
119
120
121
# File 'lib/query_packwerk/violation.rb', line 113

def sources_with_locations
  load_cache!

  files.flat_map do |file_name|
    @file_cache
      .get_full_sources(file_name: file_name, class_name: class_name)
      .map { |s| ["#{file_name}:#{s.loc.line}", s.source] }
  end
end

#to_package_nameObject



73
74
75
# File 'lib/query_packwerk/violation.rb', line 73

def to_package_name
  @original_violation.to_package_name
end

#typeObject



68
69
70
# File 'lib/query_packwerk/violation.rb', line 68

def type
  @original_violation.type
end