constant_resolver 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ inherit_from:
2
+ - https://shopify.github.io/ruby-style-guide/rubocop.yml
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 2.6
6
+ UseCache: true
7
+ CacheRootDirectory: tmp/rubocop
8
+
9
+ require: rubocop-performance
@@ -0,0 +1,76 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to make participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, sex characteristics, gender identity and expression,
9
+ level of experience, education, socio-economic status, nationality, personal
10
+ appearance, race, religion, or sexual identity and orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies within all project spaces, and it also applies when
49
+ an individual is representing the project or its community in public spaces.
50
+ Examples of representing a project or community include using an official
51
+ project e-mail address, posting via an official social media account, or acting
52
+ as an appointed representative at an online or offline event. Representation of
53
+ a project may be further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at opensource@shopify.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72
+
73
+ [homepage]: https://www.contributor-covenant.org
74
+
75
+ For answers to common questions about this code of conduct, see
76
+ https://www.contributor-covenant.org/faq
@@ -0,0 +1,3 @@
1
+ # Contributing
2
+
3
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/constant_resolver.
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in constant_resolver.gemspec
8
+ gemspec
9
+
10
+ group :deployment do
11
+ gem 'rake'
12
+ end
13
+
14
+ group :development do
15
+ gem 'rubocop', '~> 0.75.1', require: false # 0.76 currently not compatible with shopify style guide
16
+ gem 'rubocop-performance'
17
+ end
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Shopify Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,60 @@
1
+ # ConstantResolver [![Build Status](https://github.com/Shopify/constant_resolver/workflows/CI/badge.svg)](https://github.com/Shopify/constant_resolver/actions?query=workflow%3ACI)
2
+
3
+ `ConstantResolver` resolves partially qualified constant reference to the fully qualified name and the path of the file defining it. It does not load the files to do that, its inference engine purely works on file paths and constant names.
4
+
5
+ `ConstantResolver` uses the same assumptions as [Rails' code loader, `Zeitwerk`](https://github.com/fxn/zeitwerk) to infer constant locations. Please see Zeitwerk's documentation on [file structure](https://github.com/fxn/zeitwerk#file-structure) and [inflection](https://github.com/fxn/zeitwerk#zeitwerkinflector) for more information.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'constant_resolver'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install constant_resolver
22
+
23
+ ## Usage
24
+
25
+ ### Initialize the resolver
26
+
27
+ Initialize a `ConstantResolver` with a root path and load paths:
28
+
29
+ ```ruby
30
+ resolver = ConstantResolver.new(
31
+ root_path: "/app",
32
+ load_paths: [
33
+ "/app/models",
34
+ "/app/services",
35
+ ]
36
+ )
37
+ ```
38
+
39
+ ### Resolve a constant
40
+
41
+ Resolve a constant from the contents of your load paths:
42
+
43
+ ```ruby
44
+ context = resolver.resolve("Some::Nested::Model")
45
+
46
+ context.name # => "::Some::Nested::Model"
47
+ context.location # => "models/some/nested/model.rb"
48
+ ```
49
+
50
+ ## Development
51
+
52
+ After checking out the repo, run `bundle` to install dependencies. Then, run `rake test` to run the tests.
53
+
54
+ ## Contributing
55
+
56
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/constant_resolver.
57
+
58
+ ## License
59
+
60
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task(default: :test)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("../lib", __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "constant_resolver/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "constant_resolver"
9
+ spec.version = ConstantResolver::VERSION
10
+ spec.authors = ["Philip Müller"]
11
+ spec.email = ["philip.mueller@shopify.com"]
12
+
13
+ spec.summary = "Statically resolve any ruby constant"
14
+ spec.description = <<~DESCRIPTION
15
+ Given a code base that adheres to certain conventions, ConstantResolver resolves any, even partially qualified,
16
+ constant to the path of the file that defines it.
17
+ DESCRIPTION
18
+ spec.homepage = "https://github.com/Shopify/constant_resolver"
19
+ spec.license = "MIT"
20
+
21
+ if spec.respond_to?(:metadata)
22
+ spec.metadata["homepage_uri"] = spec.homepage
23
+ spec.metadata["source_code_uri"] = "https://github.com/Shopify/constant_resolver"
24
+ spec.metadata["changelog_uri"] = "https://github.com/Shopify/constant_resolver/releases"
25
+ end
26
+
27
+ # Specify which files should be added to the gem when it is released.
28
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
29
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
30
+ %x(git ls-files -z).split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
31
+ end
32
+ spec.require_paths = ["lib"]
33
+
34
+ spec.add_development_dependency("rake", "~> 10.0")
35
+ spec.add_development_dependency("minitest", "~> 5.0")
36
+ end
data/dev.yml ADDED
@@ -0,0 +1,19 @@
1
+ name: constant-resolver
2
+
3
+ type: ruby
4
+
5
+ up:
6
+ - ruby: 2.6.2
7
+ - bundler
8
+
9
+ commands:
10
+ test:
11
+ syntax:
12
+ argument: file
13
+ optional: args...
14
+ run: |
15
+ if [[ $# -eq 0 ]]; then
16
+ bundle exec rake test
17
+ else
18
+ bundle exec ruby -I test "$@"
19
+ fi
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "constant_resolver/version"
4
+
5
+ # Get information about (partially qualified) constants without loading the application code.
6
+ # We infer the fully qualified name and the filepath.
7
+ #
8
+ # The implementation makes a few assumptions about the code base:
9
+ # - `Something::SomeOtherThing` is defined in a path of either `something/some_other_thing.rb` or `something.rb`,
10
+ # relative to the load path. Constants that have their own file do not have all-uppercase names like MAGIC_NUMBER or
11
+ # all-uppercase parts like SomeID. Rails' `zeitwerk` autoloader makes the same assumption.
12
+ # - It is OK to not always infer the exact file defining the constant. For example, when a constant is inherited, we
13
+ # have no way of inferring the file it is defined in. You could argue though that inheritance means that another
14
+ # constant with the same name exists in the inheriting class, and this view is sufficient for all our use cases.
15
+ class ConstantResolver
16
+ class Error < StandardError; end
17
+ class ConstantContext < Struct.new(:name, :location); end
18
+
19
+ class DefaultInflector
20
+ def camelize(string)
21
+ string = string.sub(/^[a-z\d]*/, &:capitalize)
22
+ string.gsub!(%r{(?:_|(/))([a-z\d]*)}i) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" }
23
+ string.gsub!("/", "::")
24
+ string
25
+ end
26
+ end
27
+
28
+ private_constant :DefaultInflector
29
+
30
+ # @param root_path [String] The root path of the application to analyze
31
+ # @param load_paths [Array<String>] The autoload paths of the application.
32
+ # @param inflector [Object] Any object that implements a `camelize` function.
33
+ #
34
+ # @example usage in a Rails app
35
+ # config = Rails.application.config
36
+ # load_paths = (config.eager_load_paths + config.autoload_paths + config.autoload_once_paths)
37
+ # .map { |p| Pathname.new(p).relative_path_from(Rails.root).to_s }
38
+ # ConstantResolver.new(
39
+ # root_path: Rails.root.to_s,
40
+ # load_paths: load_paths
41
+ # )
42
+ def initialize(root_path:, load_paths:, inflector: DefaultInflector.new)
43
+ root_path += "/" unless root_path.end_with?("/")
44
+ load_paths = load_paths.map { |p| p.end_with?("/") ? p : p + "/" }.uniq
45
+
46
+ @root_path = root_path
47
+ @load_paths = load_paths
48
+ @file_map = nil
49
+ @inflector = inflector
50
+ end
51
+
52
+ # Resolve a constant via its name.
53
+ # If the name is partially qualified, we need the current namespace path to correctly infer its full name
54
+ #
55
+ # @param const_name [String] The constant's name, fully or partially qualified.
56
+ # @param current_namespace_path [Array<String>] (optional) The namespace of the context in which the constant is
57
+ # used, e.g. ["Apps", "Models"] for `Apps::Models`. Defaults to [] which means top level.
58
+ # @return [ConstantResolver::ConstantContext]
59
+ def resolve(const_name, current_namespace_path: [])
60
+ current_namespace_path = [] if const_name.start_with?("::")
61
+ inferred_name, location = resolve_constant(const_name.sub(/^::/, ""), current_namespace_path)
62
+
63
+ return unless inferred_name
64
+
65
+ ConstantContext.new(
66
+ inferred_name,
67
+ location,
68
+ )
69
+ end
70
+
71
+ # Maps constant names to file paths.
72
+ #
73
+ # @return [Hash<String, String>]
74
+ def file_map
75
+ return @file_map if @file_map
76
+ @file_map = {}
77
+ duplicate_files = {}
78
+
79
+ @load_paths.each do |load_path|
80
+ Dir[@root_path + load_path + "**/*.rb"].each do |file_path|
81
+ root_relative_path = file_path.delete_prefix!(@root_path)
82
+ const_name = @inflector.camelize(root_relative_path.delete_prefix(load_path).delete_suffix!(".rb"))
83
+ existing_entry = @file_map[const_name]
84
+
85
+ if existing_entry
86
+ duplicate_files[const_name] ||= [existing_entry]
87
+ duplicate_files[const_name] << root_relative_path
88
+ end
89
+ @file_map[const_name] = root_relative_path
90
+ end
91
+ end
92
+
93
+ unless duplicate_files.empty?
94
+ message = duplicate_files.map do |const_name, full_paths|
95
+ "ERROR: '#{const_name}' could refer to any of\n#{full_paths.map { |p| ' ' + p }.join("\n")}"
96
+ end.join("\n")
97
+ raise(Error, message)
98
+ end
99
+ raise(Error, "could not find any files") if @file_map.empty?
100
+ @file_map
101
+ end
102
+
103
+ # @api private
104
+ def config
105
+ {
106
+ root_path: @root_path,
107
+ load_paths: @load_paths,
108
+ }
109
+ end
110
+
111
+ private
112
+
113
+ def resolve_constant(const_name, current_namespace_path, original_name: const_name)
114
+ namespace, location = resolve_traversing_namespace_path(const_name, current_namespace_path)
115
+ if location
116
+ ["::" + namespace.push(original_name).join("::"), location]
117
+ elsif !const_name.include?("::")
118
+ # constant could not be resolved to a file in the given load paths
119
+ [nil, nil]
120
+ else
121
+ parent_constant = const_name.split("::")[0..-2].join("::")
122
+ resolve_constant(parent_constant, current_namespace_path, original_name: original_name)
123
+ end
124
+ end
125
+
126
+ def resolve_traversing_namespace_path(const_name, current_namespace_path)
127
+ fully_qualified_name_guess = (current_namespace_path + [const_name]).join("::")
128
+
129
+ location = file_map[fully_qualified_name_guess]
130
+ if location || fully_qualified_name_guess == const_name
131
+ [current_namespace_path, location]
132
+ else
133
+ resolve_traversing_namespace_path(const_name, current_namespace_path[0..-2])
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ConstantResolver
4
+ VERSION = "0.1.5"
5
+ end
@@ -0,0 +1,6 @@
1
+ ---
2
+ classification: library
3
+ owners:
4
+ - Shopify/component-patterns
5
+ slack_channels:
6
+ - architecture-patterns
@@ -0,0 +1 @@
1
+
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: constant_resolver
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.5
5
+ platform: ruby
6
+ authors:
7
+ - Philip Müller
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-12-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '10.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '10.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
41
+ description: |
42
+ Given a code base that adheres to certain conventions, ConstantResolver resolves any, even partially qualified,
43
+ constant to the path of the file that defines it.
44
+ email:
45
+ - philip.mueller@shopify.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - ".github/probots.yml"
51
+ - ".github/workflows/ci.yml"
52
+ - ".gitignore"
53
+ - ".rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml"
54
+ - ".rubocop.yml"
55
+ - CODE_OF_CONDUCT.md
56
+ - CONTRIBUTING.md
57
+ - Gemfile
58
+ - LICENSE.txt
59
+ - README.md
60
+ - Rakefile
61
+ - constant_resolver.gemspec
62
+ - dev.yml
63
+ - lib/constant_resolver.rb
64
+ - lib/constant_resolver/version.rb
65
+ - service.yml
66
+ - shipit.yml
67
+ homepage: https://github.com/Shopify/constant_resolver
68
+ licenses:
69
+ - MIT
70
+ metadata:
71
+ homepage_uri: https://github.com/Shopify/constant_resolver
72
+ source_code_uri: https://github.com/Shopify/constant_resolver
73
+ changelog_uri: https://github.com/Shopify/constant_resolver/releases
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubygems_version: 3.0.3
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: Statically resolve any ruby constant
93
+ test_files: []