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.



105
106
107
108
109
110
111
112
# File 'lib/archspec/model.rb', line 105

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.



103
104
105
# File 'lib/archspec/model.rb', line 103

def components
  @components
end

#constantsObject (readonly)

Returns the value of attribute constants.



103
104
105
# File 'lib/archspec/model.rb', line 103

def constants
  @constants
end

#edgesObject (readonly)

Returns the value of attribute edges.



103
104
105
# File 'lib/archspec/model.rb', line 103

def edges
  @edges
end

#filesObject (readonly)

Returns the value of attribute files.



103
104
105
# File 'lib/archspec/model.rb', line 103

def files
  @files
end

#rootObject (readonly)

Returns the value of attribute root.



103
104
105
# File 'lib/archspec/model.rb', line 103

def root
  @root
end

Instance Method Details

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



124
125
126
127
128
129
130
131
132
133
# File 'lib/archspec/model.rb', line 124

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



135
136
137
# File 'lib/archspec/model.rb', line 135

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



114
115
116
117
118
119
120
121
122
# File 'lib/archspec/model.rb', line 114

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



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/archspec/model.rb', line 154

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



250
251
252
253
254
255
256
257
258
# File 'lib/archspec/model.rb', line 250

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



242
243
244
245
246
247
248
# File 'lib/archspec/model.rb', line 242

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



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/archspec/model.rb', line 223

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



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

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



177
178
179
180
181
# File 'lib/archspec/model.rb', line 177

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



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

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

#constants_named(name) ⇒ Object



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

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

#dependency_edgesObject



191
192
193
# File 'lib/archspec/model.rb', line 191

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

#method_definitions_for_component(name) ⇒ Object



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

def method_definitions_for_component(name)
  component = components.fetch(name.to_sym)
  component.constants.flat_map { |constant_name| constants_named(constant_name) }.flat_map(&:method_definitions)
rescue KeyError
  []
end

#resolve_constant_reference(name, from_constant) ⇒ Object



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/archspec/model.rb', line 205

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)


260
261
262
# File 'lib/archspec/model.rb', line 260

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

#target_components_for(edge) ⇒ Object



195
196
197
198
199
200
201
202
203
# File 'lib/archspec/model.rb', line 195

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