Class: ConstantResolver

Inherits:
Object
  • Object
show all
Defined in:
lib/constant_resolver.rb,
lib/constant_resolver/version.rb

Overview

Get information about (partially qualified) constants without loading the application code. We infer the fully qualified name and the filepath.

The implementation makes a few assumptions about the code base:

  • ‘Something::SomeOtherThing` is defined in a path of either `something/some_other_thing.rb` or `something.rb`, relative to the load path. Constants that have their own file do not have all-uppercase names like MAGIC_NUMBER or all-uppercase parts like SomeID. Rails’ ‘zeitwerk` autoloader makes the same assumption.

  • It is OK to not always infer the exact file defining the constant. For example, when a constant is inherited, we have no way of inferring the file it is defined in. You could argue though that inheritance means that another constant with the same name exists in the inheriting class, and this view is sufficient for all our use cases.

Defined Under Namespace

Classes: ConstantContext, Error

Constant Summary collapse

VERSION =
"0.2.0"

Instance Method Summary collapse

Constructor Details

#initialize(root_path:, load_paths:, inflector: DefaultInflector.new) ⇒ ConstantResolver

Returns a new instance of ConstantResolver.

Examples:

usage in a Rails app

config = Rails.application.config
load_paths = (config.eager_load_paths + config.autoload_paths + config.autoload_once_paths)
  .map { |p| Pathname.new(p).relative_path_from(Rails.root).to_s }
ConstantResolver.new(
  root_path: Rails.root.to_s,
  load_paths: load_paths
)

Parameters:

  • root_path (String)

    The root path of the application to analyze

  • load_paths (Array<String>)

    The autoload paths of the application.

  • inflector (Object) (defaults to: DefaultInflector.new)

    Any object that implements a ‘camelize` function.



42
43
44
45
46
47
48
49
# File 'lib/constant_resolver.rb', line 42

def initialize(root_path:, load_paths:, inflector: DefaultInflector.new)
  root_path += "/" unless root_path.end_with?("/")

  @root_path = root_path
  @load_paths = coerce_load_paths(load_paths)
  @file_map = nil
  @inflector = inflector
end

Instance Method Details

#configObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



113
114
115
116
117
118
# File 'lib/constant_resolver.rb', line 113

def config
  {
    root_path: @root_path,
    load_paths: @load_paths,
  }
end

#file_mapHash<String, String>

Maps constant names to file paths.

Returns:

  • (Hash<String, String>)


73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/constant_resolver.rb', line 73

def file_map
  return @file_map if @file_map
  @file_map = {}
  duplicate_files = {}

  @load_paths.each_pair do |load_path, default_ns|
    Dir[glob_path(load_path)].each do |file_path|
      root_relative_path = file_path.delete_prefix!(@root_path)
      const_name = @inflector.camelize(root_relative_path.delete_prefix(load_path).delete_suffix!(".rb"))
      const_name = "#{default_ns}::#{const_name}" unless default_ns == "Object"
      existing_entry = @file_map[const_name]

      if existing_entry
        duplicate_files[const_name] ||= [existing_entry]
        duplicate_files[const_name] << root_relative_path
      end
      @file_map[const_name] = root_relative_path
    end
  end

  if duplicate_files.any?
    raise(Error, <<~MSG)
      Ambiguous constant definition:

      #{duplicate_files.map { |const_name, paths| ambiguous_constant_message(const_name, paths) }.join("\n")}
    MSG
  end

  if @file_map.empty?
    raise(Error, <<~MSG)
      Could not find any ruby files. Searched in:

      - #{@load_paths.keys.map { |load_path| glob_path(load_path) }.join("\n- ")}
    MSG
  end

  @file_map
end

#resolve(const_name, current_namespace_path: []) ⇒ ConstantResolver::ConstantContext

Resolve a constant via its name. If the name is partially qualified, we need the current namespace path to correctly infer its full name

Parameters:

  • const_name (String)

    The constant’s name, fully or partially qualified.

  • current_namespace_path (Array<String>) (defaults to: [])

    (optional) The namespace of the context in which the constant is used, e.g. [“Apps”, “Models”] for ‘Apps::Models`. Defaults to [] which means top level.

Returns:



58
59
60
61
62
63
64
65
66
67
68
# File 'lib/constant_resolver.rb', line 58

def resolve(const_name, current_namespace_path: [])
  current_namespace_path = [] if const_name.start_with?("::")
  inferred_name, location = resolve_constant(const_name.sub(/^::/, ""), current_namespace_path)

  return unless inferred_name

  ConstantContext.new(
    inferred_name,
    location,
  )
end