Class: ArchSpec::Graph

Inherits:
Object
  • Object
show all
Defined in:
lib/archspec/model.rb

Constant Summary collapse

DEPENDENCY_EDGE_TYPES =
%i[
  references_constant
  inherits_from
  includes
  prepends
  extends
].freeze
RESOLVED_ROOTS =
%w[Object BasicObject].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root) ⇒ Graph

Returns a new instance of Graph.



111
112
113
114
115
116
117
118
# File 'lib/archspec/model.rb', line 111

def initialize(root)
  @root = File.expand_path(root)
  @files = {}
  @constants = []
  @constants_by_name = Hash.new { |hash, key| hash[key] = [] }
  @edges = []
  @components = {}
end

Instance Attribute Details

#componentsObject (readonly)

Returns the value of attribute components.



109
110
111
# File 'lib/archspec/model.rb', line 109

def components
  @components
end

#constantsObject (readonly)

Returns the value of attribute constants.



109
110
111
# File 'lib/archspec/model.rb', line 109

def constants
  @constants
end

#edgesObject (readonly)

Returns the value of attribute edges.



109
110
111
# File 'lib/archspec/model.rb', line 109

def edges
  @edges
end

#filesObject (readonly)

Returns the value of attribute files.



109
110
111
# File 'lib/archspec/model.rb', line 109

def files
  @files
end

#rootObject (readonly)

Returns the value of attribute root.



109
110
111
# File 'lib/archspec/model.rb', line 109

def root
  @root
end

Instance Method Details

#add_constant(name:, kind:, path:, location:) ⇒ Object



130
131
132
133
134
135
136
137
138
139
# File 'lib/archspec/model.rb', line 130

def add_constant(name:, kind:, path:, location:)
  normalized = normalize_constant(name)
  existing = @constants_by_name[normalized].find { |constant| constant.path == path && constant.kind == kind }
  return existing if existing

  constant = ConstantNode.new(name: normalized, kind: kind, path: path, location: location)
  constants << constant
  @constants_by_name[normalized] << constant
  constant
end

#add_edge(type:, from_path:, from_constant:, to:, location:, confidence: :high, receiver: nil) ⇒ Object



141
142
143
# File 'lib/archspec/model.rb', line 141

def add_edge(type:, from_path:, from_constant:, to:, location:, confidence: :high, receiver: nil)
  edges << Edge.new(type, from_path, from_constant, normalize_constant(to), location, confidence, receiver)
end

#add_file(path:, expected_constant:, parse_errors:, suppressions: []) ⇒ Object



120
121
122
123
124
125
126
127
128
# File 'lib/archspec/model.rb', line 120

def add_file(path:, expected_constant:, parse_errors:, suppressions: [])
  files[path] = SourceFile.new(
    root: root,
    path: path,
    expected_constant: expected_constant,
    parse_errors: parse_errors,
    suppressions: suppressions
  )
end

#assign_components(component_specs) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/archspec/model.rb', line 160

def assign_components(component_specs)
  @components = {}

  component_specs.each do |spec|
    component = Component.new(spec.name)

    spec.file_patterns.each do |pattern|
      each_matching_file(pattern) { |path| component.add_file(path, reason: "matched file pattern #{pattern}") }
    end

    constants.each do |constant|
      matched_file = component.files.include?(constant.path)
      matched_constant = spec.matches_constant?(constant.name)
      next unless matched_file || matched_constant

      component.add_file(constant.path, reason: "defines #{constant.name}") if matched_constant
      component.add_constant(constant.name,
                             reason: matched_file ? 'defined in matched file' : 'matched namespace/constant selector')
    end

    @components[component.name] = component
  end
end

#component_assignment_reasons_for_constant(name) ⇒ Object



290
291
292
293
294
295
296
297
298
# File 'lib/archspec/model.rb', line 290

def component_assignment_reasons_for_constant(name)
  normalized = normalize_constant(name)

  components.values.each_with_object({}) do |component, reasons|
    next unless component.constants.include?(normalized)

    reasons[component.name] = component.constant_reasons[normalized].to_a.sort
  end
end

#component_assignment_reasons_for_path(path) ⇒ Object



282
283
284
285
286
287
288
# File 'lib/archspec/model.rb', line 282

def component_assignment_reasons_for_path(path)
  components.values.each_with_object({}) do |component, reasons|
    next unless component.files.include?(path)

    reasons[component.name] = component.file_reasons[path].to_a.sort
  end
end

#component_dependency_pairs(only: nil) ⇒ Object



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/archspec/model.rb', line 263

def component_dependency_pairs(only: nil)
  allowed_sources = Array(only).compact.map(&:to_sym).to_set
  pairs = Set.new

  dependency_edges.each do |edge|
    source_components = component_names_for_path(edge.from_path)
    source_components &= allowed_sources unless allowed_sources.empty?
    next if source_components.empty?

    target_components_for(edge).each do |target|
      source_components.each do |source|
        pairs.add([source, target]) unless source == target
      end
    end
  end

  pairs
end

#component_names_for_constant(name) ⇒ Object



190
191
192
193
194
195
196
# File 'lib/archspec/model.rb', line 190

def component_names_for_constant(name)
  normalized = normalize_constant(name)

  components.values.each_with_object(Set.new) do |component, names|
    names.add(component.name) if component.constants.include?(normalized)
  end
end

#component_names_for_path(path) ⇒ Object



184
185
186
187
188
# File 'lib/archspec/model.rb', line 184

def component_names_for_path(path)
  components.values.each_with_object(Set.new) do |component, names|
    names.add(component.name) if component.files.include?(path)
  end
end

#constants_for_path(path) ⇒ Object



149
150
151
# File 'lib/archspec/model.rb', line 149

def constants_for_path(path)
  constants.select { |constant| constant.path == path }
end

#constants_named(name) ⇒ Object



145
146
147
# File 'lib/archspec/model.rb', line 145

def constants_named(name)
  @constants_by_name[normalize_constant(name)]
end

#dependency_edgesObject



198
199
200
# File 'lib/archspec/model.rb', line 198

def dependency_edges
  edges.select { |edge| DEPENDENCY_EDGE_TYPES.include?(edge.type) }
end

#effective_instance_methods(name, visited = Set.new) ⇒ Object

Instance methods a constant responds to, walking resolvable superclasses and include/prepend mixins. Returns [methods, unresolved ancestor names]; a non-empty second element means the answer is incomplete.



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/archspec/model.rb', line 233

def effective_instance_methods(name, visited = Set.new)
  normalized = normalize_constant(name)
  return [Set.new, Set.new] if visited.include?(normalized)

  visited.add(normalized)
  return [Set.new, Set.new] if RESOLVED_ROOTS.include?(normalized)

  nodes = constants_named(normalized)
  return [Set.new, Set[normalized]] if nodes.empty?

  methods = Set.new
  unresolved = Set.new

  nodes.each do |node|
    methods.merge(node.instance_methods)

    ancestors = node.mixins[:include].to_a + node.mixins[:prepend].to_a
    ancestors << node.superclass if node.superclass

    ancestors.each do |ancestor|
      resolved_name = resolve_constant_reference(ancestor, node.name)
      ancestor_methods, ancestor_unresolved = effective_instance_methods(resolved_name, visited)
      methods.merge(ancestor_methods)
      unresolved.merge(ancestor_unresolved)
    end
  end

  [methods, unresolved]
end

#method_definitions_for_component(name) ⇒ Object



153
154
155
156
157
158
# File 'lib/archspec/model.rb', line 153

def method_definitions_for_component(name)
  component = components[name.to_sym]
  return [] unless component

  component.constants.flat_map { |constant_name| constants_named(constant_name) }.flat_map(&:method_definitions)
end

#resolve_constant_reference(name, from_constant) ⇒ Object



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/archspec/model.rb', line 212

def resolve_constant_reference(name, from_constant)
  normalized = normalize_constant(name)
  candidates = []

  if from_constant
    namespace = normalize_constant(from_constant).split('::')
    namespace.pop

    until namespace.empty?
      candidates << "#{namespace.join('::')}::#{normalized}"
      namespace.pop
    end
  end

  candidates << normalized
  candidates.find { |candidate| constants_named(candidate).any? } || normalized
end

#suppressed?(diagnostic) ⇒ Boolean

Returns:

  • (Boolean)


300
301
302
# File 'lib/archspec/model.rb', line 300

def suppressed?(diagnostic)
  files[diagnostic.location.path]&.suppressions&.any? { |suppression| suppression.matches?(diagnostic) }
end

#target_components_for(edge) ⇒ Object



202
203
204
205
206
207
208
209
210
# File 'lib/archspec/model.rb', line 202

def target_components_for(edge)
  return Set.new unless DEPENDENCY_EDGE_TYPES.include?(edge.type)

  resolved = resolve_constant_reference(edge.to, edge.from_constant)
  constants_named(resolved).each_with_object(Set.new) do |constant, names|
    names.merge(component_names_for_path(constant.path))
    names.merge(component_names_for_constant(constant.name))
  end
end