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.3.0"

Instance Method Summary collapse

Constructor Details

#initialize(root_path:, load_paths:, exclude: [], 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
)


46
47
48
49
50
51
52
53
54
# File 'lib/constant_resolver.rb', line 46

def initialize(root_path:, load_paths:, exclude: [], 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
  @exclude = exclude
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.



122
123
124
125
126
127
# File 'lib/constant_resolver.rb', line 122

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

#file_mapHash<String, String>

Maps constant names to file paths.



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
111
112
113
114
115
116
117
118
119
# File 'lib/constant_resolver.rb', line 78

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

      if allowed?(root_relative_path)
        @file_map[const_name] = root_relative_path
      end
    end
  end

  if duplicate_files.any?
    raise(Error, "      Ambiguous constant definition:\n\n      \#{duplicate_files.map { |const_name, paths| ambiguous_constant_message(const_name, paths) }.join(\"\\n\")}\n    MSG\n  end\n\n  if @file_map.empty?\n    raise(Error, <<~MSG)\n      Could not find any ruby files. Searched in:\n\n      - \#{@load_paths.keys.map { |load_path| glob_path(load_path) }.join(\"\\n- \")}\n    MSG\n  end\n\n  @file_map\nend\n")

#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



63
64
65
66
67
68
69
70
71
72
73
# File 'lib/constant_resolver.rb', line 63

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