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

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root) ⇒ Graph

Returns a new instance of Graph.



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

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.



107
108
109
# File 'lib/archspec/model.rb', line 107

def components
  @components
end

#constantsObject (readonly)

Returns the value of attribute constants.



107
108
109
# File 'lib/archspec/model.rb', line 107

def constants
  @constants
end

#edgesObject (readonly)

Returns the value of attribute edges.



107
108
109
# File 'lib/archspec/model.rb', line 107

def edges
  @edges
end

#filesObject (readonly)

Returns the value of attribute files.



107
108
109
# File 'lib/archspec/model.rb', line 107

def files
  @files
end

#rootObject (readonly)

Returns the value of attribute root.



107
108
109
# File 'lib/archspec/model.rb', line 107

def root
  @root
end

Instance Method Details

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



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

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) ⇒ Object



139
140
141
# File 'lib/archspec/model.rb', line 139

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

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



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

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



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

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



255
256
257
258
259
260
261
262
263
# File 'lib/archspec/model.rb', line 255

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



247
248
249
250
251
252
253
# File 'lib/archspec/model.rb', line 247

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



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/archspec/model.rb', line 228

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



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

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



182
183
184
185
186
# File 'lib/archspec/model.rb', line 182

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



147
148
149
# File 'lib/archspec/model.rb', line 147

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

#constants_named(name) ⇒ Object



143
144
145
# File 'lib/archspec/model.rb', line 143

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

#dependency_edgesObject



196
197
198
# File 'lib/archspec/model.rb', line 196

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

#method_definitions_for_component(name) ⇒ Object



151
152
153
154
155
156
# File 'lib/archspec/model.rb', line 151

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



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

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)


265
266
267
# File 'lib/archspec/model.rb', line 265

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

#target_components_for(edge) ⇒ Object



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

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