partial_finder 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d2829293afb5eb8e9b04ebd9a69aad5156bf2f3a6c0124cb2242942dc08dbb6c
4
+ data.tar.gz: 7f023fefc32b9233557a5dfd3887bc9f617a86bfcc62a27a0dada85922c63c42
5
+ SHA512:
6
+ metadata.gz: 5c807fdddf4dda8fbaa87429266829bd5828e139ebb2a1d8e452b00c91f5e90181dc160f295e03ce946b379baa359e02d0e68293e3c17dad03545ffabdd67fc8
7
+ data.tar.gz: 93cfcd44e0b040d447789811b9b21722cc4989fd42645413e7f343a18781d1c7b53235bb799927718d74264e18b34b2882c64265393ee52c624a39109fff3cad
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ .byebug_history
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,74 @@
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 making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ 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 both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ 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 ironclad00@gmail.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 [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in partial_finder.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,53 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ partial_finder (0.1.2)
5
+ activesupport (~> 6)
6
+ colorize (>= 0.8)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activesupport (6.1.4.4)
12
+ concurrent-ruby (~> 1.0, >= 1.0.2)
13
+ i18n (>= 1.6, < 2)
14
+ minitest (>= 5.1)
15
+ tzinfo (~> 2.0)
16
+ zeitwerk (~> 2.3)
17
+ byebug (11.1.3)
18
+ colorize (0.8.1)
19
+ concurrent-ruby (1.1.10)
20
+ diff-lcs (1.5.0)
21
+ i18n (1.10.0)
22
+ concurrent-ruby (~> 1.0)
23
+ minitest (5.15.0)
24
+ rake (13.0.6)
25
+ rspec (3.10.0)
26
+ rspec-core (~> 3.10.0)
27
+ rspec-expectations (~> 3.10.0)
28
+ rspec-mocks (~> 3.10.0)
29
+ rspec-core (3.10.1)
30
+ rspec-support (~> 3.10.0)
31
+ rspec-expectations (3.10.1)
32
+ diff-lcs (>= 1.2.0, < 2.0)
33
+ rspec-support (~> 3.10.0)
34
+ rspec-mocks (3.10.2)
35
+ diff-lcs (>= 1.2.0, < 2.0)
36
+ rspec-support (~> 3.10.0)
37
+ rspec-support (3.10.3)
38
+ tzinfo (2.0.4)
39
+ concurrent-ruby (~> 1.0)
40
+ zeitwerk (2.5.3)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ bundler (~> 2.3.4)
47
+ byebug
48
+ partial_finder!
49
+ rake (>= 12.3.3)
50
+ rspec (~> 3.0)
51
+
52
+ BUNDLED WITH
53
+ 2.3.9
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Jeremy Baker
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.
data/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # PartialFinder
2
+
3
+ As Rails apps grow, partial usage and templates get increasingly complicated. PartialFinder adds rake tasks to your Rails app to help you track down the various ways that a given parial may be rendered. You can provide it a partial path and it will output all of the routes and controllers that serve it, along with the intermediate files.
4
+
5
+ Usage: `rake partial_finder:find\['path/to/_partial.html.erb'\]`
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile under the development group:
10
+
11
+ ```ruby
12
+ gem 'partial_finder', "~> 0.1.1", git: "https://github.com/Negotiatus/Partial-Finder.git"
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ ## Usage
20
+
21
+ To view this help manual outside of the README, run `bundle exec rake partial_finder:help`.
22
+
23
+ PartialFinder adds two rake tasks to help track down partial usage:
24
+
25
+ Task: Find
26
+ Usage: `rake partial_finder:find\['path/to/_partial.html.erb'\]`
27
+ Outputs all render chains and tries to match each partial with any controllers and routes that eventually render it.
28
+
29
+ Task: Debug
30
+ Usage: `rake partial_finder:debug\['path/to/_partial.html.erb'\]`
31
+ Contains the same output as Find but with additional intermediate steps that can be used to help validate the final results.
32
+
33
+ Here's an example output when running `rake partial_finder:find\["app/views/order_product_links/_description_block.html.erb"\]`:
34
+
35
+ ![example_results](./example.png)
36
+
37
+
38
+ ## Development
39
+
40
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
41
+
42
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
43
+
44
+ ## Contributing
45
+
46
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Negotiatus/Partial-Finder. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
47
+
48
+ ## License
49
+
50
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
51
+
52
+ ## Code of Conduct
53
+
54
+ Everyone interacting in the PartialFinder project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/Negotiatus/Partial-Finder/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'partial_finder'
2
+
3
+ namespace :partial_finder do
4
+
5
+ desc "Output routes that render a given partial. Usage: rake partial_finder:find\\['path/to/_partial.html.erb'\\]"
6
+ task :find, [:path] => [:environment] do |task_name, args|
7
+ args[:path]
8
+ end
9
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "partial_finder"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/rake ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rake", "rake")
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/example.png ADDED
Binary file
@@ -0,0 +1,107 @@
1
+ module PartialFinder
2
+ # AssumptionGraph accepts a standard Graph class and augments it with more information about
3
+ # how each render chain terminates, ie what controller method is used and what route points
4
+ # to said controller method.
5
+ #
6
+ # Given the flexiblity of metaprogramming and the many edge cases that exist even in a
7
+ # system with good conventions like the Rails rendering system, it's best to consider the
8
+ # guesses that this class makes exactly that - guesses, and sometimes manual checking may be
9
+ # needed to ensure the correctness of it's output.
10
+ #
11
+ # See Graph and LinkSet for more information on the primitives that this class is based on.
12
+ class AssumptionGraph < Graph
13
+ attr_reader :custom_rails_root
14
+
15
+ # Custom rails root needs to be set if the Rails root and current working directory
16
+ # of this class are not the same. This is needed to properly load and read controller
17
+ # code. Usually they will be the same, but during testing or when used outside of the
18
+ # standard rake context, may need to be set.
19
+ def initialize(links, custom_rails_root = nil)
20
+ super(links)
21
+ @custom_rails_root = custom_rails_root
22
+ @structure = structure.map{ |link| add_assumptions_to(link) }
23
+ end
24
+
25
+ private
26
+
27
+ def add_assumptions_to(link)
28
+ if link.parent.is_a? Array
29
+ link.parent.each{ |li| add_assumptions_to(li) }
30
+ else
31
+ link.parent = assumptions_for(link)
32
+ end
33
+
34
+ link
35
+ end
36
+
37
+ def assumptions_for(link)
38
+ case Formatter.type_of(link.parent)
39
+ when :partial
40
+ new_parent = "This render chain appears to be unused".colorize(:yellow)
41
+ when :view
42
+ new_parent = new_parent_for_view(link.parent)
43
+ when :controller
44
+ new_parent = new_parent_for_controller(link)
45
+ else
46
+ new_parent = "Match of unknown type found in #{link.parent}".colorize(:yellow)
47
+ end
48
+
49
+ [Link.new(link.parent, new_parent)]
50
+ end
51
+
52
+ def new_parent_for_controller(link)
53
+ if custom_rails_root.present?
54
+ methods = Formatter.methods_that_render(link.child, link.parent, custom_rails_root)
55
+ else
56
+ methods = Formatter.methods_that_render(link.child, link.parent)
57
+ end
58
+
59
+ if methods.any?
60
+ sigs = methods
61
+ .map{ |method| Formatter.controller_signature(link.parent, method) }
62
+
63
+ sig_str = sigs
64
+ .join(', ')
65
+ .remove(/,\z/)
66
+ sig_msg = "Rendered by #{sig_str}".colorize(:green)
67
+
68
+ routes = sigs.flat_map{ |sig| Router.routes_from(sig) }
69
+
70
+ if routes.any?
71
+ routes = routes
72
+ .join(', ')
73
+ .remove(/,\z/)
74
+ route_msg = "Routes to #{routes}".colorize(:green)
75
+ else
76
+ route_msg = "No routes found".colorize(:red)
77
+ end
78
+ else
79
+ sig_msg = "Method lookup failed".colorize(:red)
80
+ route_msg = "Routing not possible since method lookup failed".colorize(:red)
81
+ end
82
+
83
+ [Link.new(sig_msg, route_msg)]
84
+ end
85
+
86
+ def new_parent_for_view(path)
87
+ # TODO: technically a view can be rendered via calls like
88
+ # render template, but this isn't accounted for
89
+ # TODO: A controller might not actually have the method
90
+ # it is assumed to have. Check this eventually
91
+ # TODO: mailers are not accounted for
92
+ sig = Formatter.controller_signature_from_view(path)
93
+ routes = Router.routes_from(sig)
94
+
95
+ if routes.any?
96
+ routes = routes
97
+ .join(', ')
98
+ .remove(/,\z/)
99
+ route_msg = "Routes to #{routes}".colorize(:green)
100
+ else
101
+ route_msg = "Could not find route for #{sig}".colorize(:red)
102
+ end
103
+
104
+ [Link.new("Rendered by #{sig}".colorize(:green), route_msg)]
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,124 @@
1
+ module PartialFinder
2
+ module Formatter
3
+ # Takes a file path and turns it into a partial reference. Example:
4
+ # app/views/orders/_order_sidepanel.html.erb
5
+ # becomes
6
+ # orders/order_sidepanel
7
+ # which is the format used when rendering a partial in a controller or view.
8
+ def self.path_to_ref(path)
9
+ ref = path.deep_dup
10
+ ref.remove!('app/').remove!('views/').remove!(/\.html\.erb\z/)
11
+ ref = ref.split('/')
12
+ ref[-1] = ref.last.remove(/\A_/)
13
+ ref.join('/')
14
+ end
15
+
16
+ def self.is_partial?(path)
17
+ return false unless path.present?
18
+ !!path.split('/').last.match(/\A_.+\.erb/)
19
+ end
20
+
21
+ def self.is_view?(path)
22
+ return false unless path.present?
23
+ !!path.split('/').last.match(/\A[^_].+\.erb/)
24
+ end
25
+
26
+ def self.is_controller?(path)
27
+ return false unless path.present?
28
+ !!path.split('/').last.match(/.+_controller\.rb/)
29
+ end
30
+
31
+ # Takes a view path that can be complete or have missing/extra
32
+ # parts and returns it in the format
33
+ # app/views/any_subfolders/view_file.html.erb
34
+ # or
35
+ # app/controllers/any_subfolders/controller_file.html.erb
36
+ #
37
+ # This is needed mainly when the search directly for grep is altered
38
+ # and parts of the path need to be restored and scrubbed of the
39
+ # leading ./ characters.
40
+ def self.fix_path(incomplete_path)
41
+ path = incomplete_path.remove(/\A\.\//)
42
+
43
+ if path.match(/\Aviews/) || path.match(/\Acontrollers/)
44
+ "app/#{path}"
45
+ elsif !path.match /\Aapp/
46
+ if is_view?(path)
47
+ "app/views/#{path}"
48
+ elsif is_controller?(path)
49
+ "app/controllers/#{path}"
50
+ else
51
+ path
52
+ end
53
+ else
54
+ path
55
+ end
56
+ end
57
+
58
+ # Determines if the given path is a partial, a view, a controller
59
+ # or none of the above
60
+ def self.type_of(path)
61
+ if is_partial?(path)
62
+ :partial
63
+ elsif is_view?(path)
64
+ :view
65
+ elsif is_controller?(path)
66
+ :controller
67
+ else
68
+ :unknown
69
+ end
70
+ end
71
+
72
+ # Given a view path, the controller name and method that implicitly
73
+ # renders it are assumed by convention. A controller signature is returned.
74
+ # If the path is not a view, an empty string is returned.
75
+ def self.controller_signature_from_view(path)
76
+ if is_view?(path)
77
+ cname = path.deep_dup
78
+ cname.remove!('app/').remove!('views/').remove!('.html.erb')
79
+ cname = cname.split('/')
80
+ method = cname.pop
81
+ "#{cname.join('/')}##{method}"
82
+ else
83
+ ""
84
+ end
85
+ end
86
+
87
+ # Returns a controller signature constructed from a controller's view path
88
+ # and a manually specified method name.
89
+ # If the path is not a controller, an empty string is returned.
90
+ def self.controller_signature(path, method)
91
+ if is_controller?(path)
92
+ cname = path.deep_dup
93
+ cname.remove!('app/').remove!('controllers/').remove!('_controller.rb')
94
+ "#{cname}##{method}"
95
+ else
96
+ ""
97
+ end
98
+ end
99
+
100
+ # Searches through a controller's definition to find which method is rendering
101
+ # a given partial. Multiple method names may be returned.
102
+ #
103
+ # A root is needed to provide flexibility so the controller file can be opened
104
+ # regardless of the current working directory. Normally paths are just used
105
+ # for their conventions, but in the controller's case here, the path needs
106
+ # to actually point to a file relative to the current working directory.
107
+ def self.methods_that_render(partial_path, controller_path, rails_root = PartialFinder.default_root)
108
+ if is_partial?(partial_path) && is_controller?(controller_path)
109
+ full_c_path = "#{rails_root}/#{controller_path}"
110
+ fragments = File.read(full_c_path).split /def (.+?)$/
111
+ ref = path_to_ref(partial_path)
112
+ matches = []
113
+
114
+ fragments.each.with_index do |fr,i|
115
+ matches << fragments[i-1] if fr.match ref
116
+ end
117
+
118
+ matches
119
+ else
120
+ ""
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,63 @@
1
+ module PartialFinder
2
+ # The Graph object assembles individual chain links into a structure that better
3
+ # resembles the multiple render paths of a partial.
4
+ #
5
+ # Specifically, using hash syntax, the structure looks something like:
6
+ # { original_partial => [{ parent1 => [...] }, { parent2 => [...] }, "terminating_view_file"] }
7
+ # In this example, the partial is directly rendered by "terminating_view_file" and
8
+ # has a deeper rendering chain up to 2 other parents somewhere.
9
+ #
10
+ # Chains will terminate in a string instead of an array (ie, the parent will be a string).
11
+ # These strings presumably will be either a controller or a view, but if the partial is unused, it could
12
+ # also terminate in itself or another partial.
13
+ class Graph
14
+ attr_reader :links, :structure
15
+
16
+ def initialize(links)
17
+ raise NonLinkArgument.new(links) unless links.is_a? LinkSet
18
+ @links = links
19
+
20
+ if links.any?
21
+ # The usage of 'root' is a side-effect of how #assemble_links works.
22
+ # If given the initial link instead of a new 'root' link, only the first
23
+ # parent of { partial => [parent1, parent2, ...] } will be traversed.
24
+ @structure = assemble_links(Link.new('root', links.first.child)).parent
25
+ else
26
+ @structure = []
27
+ end
28
+ end
29
+
30
+ def self.from(path, root)
31
+ new(LinkSet.new(path,root))
32
+ end
33
+
34
+ def to_s
35
+ @to_s ||= Printer.new(self).string
36
+ end
37
+
38
+ private
39
+
40
+ # This is also where controller methods, routing, and looping messages are
41
+ # determined.
42
+ def assemble_links(origin_link)
43
+ found = []
44
+
45
+ links.each do |link|
46
+ # If the given arg link is rendered by the link being examined,
47
+ # it's part of the render chain.
48
+ # Ie, if origin_link = { partialC => partialB }, then link
49
+ # { partialB => partialA } is a found link.
50
+ found << link.deep_dup if link.child == origin_link.parent
51
+ end
52
+
53
+ # If no links were found, return the new { link => parent_set }
54
+ # that was just built. Otherwise, recurse through the remaining parent
55
+ # links to continue building the render chain.
56
+ if found.any?
57
+ origin_link.parent = found.map{ |link| assemble_links(link) }
58
+ end
59
+
60
+ origin_link
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,6 @@
1
+ # Child should be a string, but parent can be a string or an array
2
+ PartialFinder::Link = Struct.new(:child, :parent) do
3
+ def to_s
4
+ "#{child} rendered by #{parent}"
5
+ end
6
+ end
@@ -0,0 +1,86 @@
1
+ require 'forwardable'
2
+
3
+ module PartialFinder
4
+ # LinkSet is a simple collection of { render_child => render_parent } file paths.
5
+ # Given a partial path and a file root to search, it will recursively search for
6
+ # and collect render links for that partial.
7
+ # This is the base structure that is used to generate and print full render chains.
8
+ class LinkSet
9
+ extend Forwardable
10
+ attr_reader :path, :search_root, :values, :debug_mode
11
+
12
+ delegate [:any?, :each, :map, :[], :first] => :@values
13
+
14
+ # Accepts a file path to a partial and a file path used as the search root.
15
+ # The search root can be expanded or shrunk as needed but should stay within
16
+ # the Rails root. It can be flexible since the size of the search directory
17
+ # can drastically effect the performance of grep.
18
+ # It is recommended to use rails_root/app.
19
+ def initialize(partial_path, search_root, debug_mode: false)
20
+ raise NonPartialArgument.new(partial_path) unless Formatter.is_partial?(partial_path)
21
+
22
+ @debug_mode = debug_mode
23
+ @path = partial_path
24
+ @search_root = search_root
25
+ @values = []
26
+ collect_links(path)
27
+ end
28
+
29
+ # Returns a list of files that reference the given partial.
30
+ # Non-partials are not searched for as render chains
31
+ # terminate in non-partials (ie, if a view or controller has
32
+ # been found, the render chain can halt).
33
+ def self.files_that_reference(path, search_root, debug_mode: false)
34
+ if Formatter.is_partial? path
35
+ # Scans for instances of the partial being explicitly rendered.
36
+ # For example, given the path app/views/orders/_foo.html.erb, the
37
+ # resulting string used by grep would be:
38
+ # "partial: [\"']orders/foo[\"']"
39
+ # This accounts for use of " and ' in the reference.
40
+ #
41
+ # TODO: Make this single line? Character escaping this properly isn't fun
42
+ term = <<~STR.remove("\n")
43
+ "partial: [\\"']#{Formatter.path_to_ref(path)}[\\"']"
44
+ STR
45
+
46
+ puts "Running grep: 'cd #{search_root} && grep -rl #{term}'" if debug_mode
47
+ `cd #{search_root} && grep -rl #{term}`
48
+ .split("\n")
49
+ .map{ |a| Formatter.fix_path(a) }
50
+ else
51
+ []
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def files_that_reference(path)
58
+ self.class.files_that_reference(path, search_root, debug_mode: debug_mode)
59
+ end
60
+
61
+ def links_are_unique?
62
+ values.uniq.size == values.size
63
+ end
64
+
65
+ # Recursive method that returns a list of
66
+ # { render_child_path => render_parent_path }
67
+ # file paths. These paths are the core structure used later on to assemble a
68
+ # full graph-like structure to represent render chains.
69
+ #
70
+ # "Parent" and "child" refer to the order that a view is rendered. The
71
+ # contorller/view that eventually renders a partial is always a parent and
72
+ # the given partial is a child.
73
+ # Ie, { file_that_is_rendered => file_that_renders_it }.
74
+ def collect_links(path)
75
+ files_that_reference(path).each do |parent_path|
76
+ values << Link.new(path, parent_path)
77
+
78
+ # Only continue recursion if there's more partials to look through. A view
79
+ # or controller indicates the end of a render chain.
80
+ collect_links(parent_path) if Formatter.is_partial?(parent_path) && links_are_unique?
81
+
82
+ values.pop unless links_are_unique?
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,39 @@
1
+ module PartialFinder
2
+ # Printer class can accept and stringify any class that implements #structure such that
3
+ # #structure returns an array of Links.
4
+ class Printer
5
+ attr_reader :structure, :string
6
+
7
+ TAB = " ".freeze
8
+
9
+ # Can be any class that has a #structure method
10
+ def initialize(graph)
11
+ @structure = graph.structure
12
+ @string = ""
13
+ structure.map{ |link| stringify_link(link) }
14
+ end
15
+
16
+ private
17
+
18
+ def stringify_link(link, depth=0)
19
+ if link.parent.is_a? Array
20
+ @string << indent(depth) + link.child + "\n"
21
+ link.parent.each{ |plink| stringify_link(plink, depth+1) }
22
+ else
23
+ @string << stringify_simple_link(link, depth)
24
+ end
25
+ end
26
+
27
+ # Simple link meaning the link parent is not an array
28
+ def stringify_simple_link(link, depth)
29
+ indent(depth) + link.child + "\n" + indent(depth+1) + link.parent + "\n"
30
+ end
31
+
32
+ # Returns a string of spaces to represent an indent
33
+ def indent(depth)
34
+ spaces = ""
35
+ depth.times{ spaces << TAB }
36
+ spaces
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,12 @@
1
+ require 'rails'
2
+
3
+ module PartialFinder
4
+ class Railtie < Rails::Railtie
5
+ railtie_name :partial_finder
6
+
7
+ rake_tasks do
8
+ path = File.expand_path(__dir__)
9
+ Dir.glob("#{path}/../tasks/**/*.rake").each{ |f| load f }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ module PartialFinder
2
+ module Router
3
+ def self.routes
4
+ @@routes ||= `rake routes`
5
+ end
6
+
7
+ # Input string must be in the format controller_name#method,
8
+ # ie "users_controller#create".
9
+ # Returns a list of strings that correspond to the routes that
10
+ # point to the given controller.
11
+ def self.routes_from(controller_sig)
12
+ entries = routes
13
+ .scan(/^.+ \/(.+)\(\.:format\) +(#{controller_sig})/)
14
+ .first
15
+
16
+ if entries.present?
17
+ entries
18
+ .reject{ |a| a == controller_sig }
19
+ .map{ |a| "/#{a}" }
20
+ else
21
+ []
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,58 @@
1
+ module PartialFinder
2
+ class Runner
3
+ attr_reader :partial, :root
4
+
5
+ def initialize(partial, search_root = nil)
6
+ @root = search_root || default_search_root
7
+ @partial = partial
8
+ end
9
+
10
+ def print
11
+ puts "\n\nStarting the partial finder for #{partial} in #{@root}\n\n".colorize(:green)
12
+
13
+ Printer.new(
14
+ AssumptionGraph.from(@partial, @root)
15
+ ).string
16
+ end
17
+
18
+ def debug
19
+ puts "\n\nStarting the partial finder in debug mode for #{partial} in #{@root}\n\n".colorize(:green)
20
+
21
+ links = LinkSet.new(@partial, @root, debug_mode: true)
22
+ graph = Graph.new(links)
23
+ agraph = AssumptionGraph.new(links)
24
+
25
+ puts ""
26
+ puts "=== Set of Links ===".colorize(:blue)
27
+ puts links.map{ |li| li.to_s }.join("\n")
28
+ puts ""
29
+ puts "=== Chains without Assumptions ===".colorize(:blue)
30
+ puts Printer.new(graph).string
31
+ puts ""
32
+ puts "=== Full Render Chains ===".colorize(:blue)
33
+ puts Printer.new(agraph).string
34
+ end
35
+
36
+ def default_search_root
37
+ PartialFinder.default_root + "/app"
38
+ end
39
+
40
+ def self.help
41
+ <<-MSG.colorize(:blue)
42
+ Partial Finder is a tool that helps link a given partial with the controllers and routes that render it. It works by making assumptions about Rail's rendering conventions and greping through the application code and routes.
43
+
44
+ Conditional rendering logic is not considered. If a view, partial, or controller appears in the output list, this is only a statement that said file MAY render the partial under certain conditions.
45
+
46
+ While it should handle all common use cases, it doesn't account for every possible edge case and manual greping may still be needed.
47
+
48
+ Task: Find
49
+ Usage: rake partial_finder:find\\['path/to/_partial.html.erb'\\]
50
+ Outputs all render chains and tries to match each partial with any controllers and routes that eventually render it.
51
+
52
+ Task: Debug
53
+ Usage: rake partial_finder:debug\\['path/to/_partial.html.erb'\\]
54
+ Contains the same output as Find but with additional intermediate steps that can be used to help validate the final results.
55
+ MSG
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,3 @@
1
+ module PartialFinder
2
+ VERSION = "0.1.2"
3
+ end
@@ -0,0 +1,41 @@
1
+ require "partial_finder/version"
2
+ require "partial_finder/formatter"
3
+ require "partial_finder/router"
4
+ require "partial_finder/runner"
5
+ require "partial_finder/printer"
6
+ require "partial_finder/link"
7
+ require "partial_finder/link_set"
8
+ require "partial_finder/graph"
9
+ require "partial_finder/assumption_graph"
10
+ require "partial_finder/railtie" if defined?(Rails)
11
+ require "colorize"
12
+ require "active_support/core_ext/object"
13
+ require "active_support/core_ext/string"
14
+
15
+ module PartialFinder
16
+ class NonPartialArgument < StandardError
17
+ def initialize(path)
18
+ super "You may only use this class with partials, but gave '#{path}'"
19
+ end
20
+ end
21
+
22
+ class NonLinkArgument < StandardError
23
+ def initialize(arg)
24
+ super "You may only use this class with a LinkSet, but gave '#{arg}'"
25
+ end
26
+ end
27
+
28
+ class NonGraphArgument < StandardError
29
+ def initialize(arg)
30
+ super "You may only use this class with a Graph, but gave '#{arg}'"
31
+ end
32
+ end
33
+
34
+ def self.default_root
35
+ if defined?(Rails)
36
+ "Rails".constantize.root.to_s
37
+ else
38
+ "."
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,6 @@
1
+ namespace :partial_finder do
2
+ desc "Same as the find task, but prints out intermediate steps as well."
3
+ task :debug, [:path] => [:environment] do |task_name, args|
4
+ puts PartialFinder::Runner.new(args[:path]).debug
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ namespace :partial_finder do
2
+ desc "Outputs routes that render a given partial."
3
+ task :find, [:path] => [:environment] do |task_name, args|
4
+ puts PartialFinder::Runner.new(args[:path]).print
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ namespace :partial_finder do
2
+ desc "Display the help menu for Partial Finder"
3
+ task help: :environment do |task_name, args|
4
+ puts PartialFinder::Runner.help
5
+ end
6
+ end
@@ -0,0 +1,45 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "partial_finder/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "partial_finder"
7
+ spec.version = PartialFinder::VERSION
8
+ spec.authors = ["Jeremy Baker"]
9
+ spec.email = ["jeremy.baker@order.co"]
10
+
11
+ spec.summary = %q{Finds app routes and render paths given a partial file name.}
12
+ spec.description = %q{Adds a rake task that accepts a view partial file name and outputs the render chain along with best guesses as to the routes used to render the partial.}
13
+ spec.homepage = "https://github.com/Negotiatus/Partial-Finder"
14
+ spec.license = "MIT"
15
+
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
18
+ if spec.respond_to?(:metadata)
19
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
20
+
21
+ spec.metadata["homepage_uri"] = spec.homepage
22
+ spec.metadata["source_code_uri"] = "https://github.com/Negotiatus/Partial-Finder"
23
+ spec.metadata["changelog_uri"] = "https://github.com/Negotiatus/Partial-Finder"
24
+ else
25
+ raise "RubyGems 2.0 or newer is required to protect against " \
26
+ "public gem pushes."
27
+ end
28
+
29
+ # Specify which files should be added to the gem when it is released.
30
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
31
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
32
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
33
+ end
34
+ spec.bindir = "exe"
35
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
36
+ spec.require_paths = ["lib"]
37
+
38
+ spec.add_development_dependency "bundler", "~> 2.3.4"
39
+ spec.add_development_dependency "rake", ">= 12.3.3"
40
+ spec.add_development_dependency "rspec", "~> 3.0"
41
+ spec.add_development_dependency "byebug"
42
+
43
+ spec.add_dependency "activesupport", "~> 6"
44
+ spec.add_dependency "colorize", ">= 0.8"
45
+ end
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: partial_finder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Jeremy Baker
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-04-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.3.4
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.3.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 12.3.3
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 12.3.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '6'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '6'
83
+ - !ruby/object:Gem::Dependency
84
+ name: colorize
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0.8'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0.8'
97
+ description: Adds a rake task that accepts a view partial file name and outputs the
98
+ render chain along with best guesses as to the routes used to render the partial.
99
+ email:
100
+ - jeremy.baker@order.co
101
+ executables: []
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - ".rspec"
107
+ - CODE_OF_CONDUCT.md
108
+ - Gemfile
109
+ - Gemfile.lock
110
+ - LICENSE.txt
111
+ - README.md
112
+ - Rakefile
113
+ - bin/console
114
+ - bin/rake
115
+ - bin/setup
116
+ - example.png
117
+ - lib/partial_finder.rb
118
+ - lib/partial_finder/assumption_graph.rb
119
+ - lib/partial_finder/formatter.rb
120
+ - lib/partial_finder/graph.rb
121
+ - lib/partial_finder/link.rb
122
+ - lib/partial_finder/link_set.rb
123
+ - lib/partial_finder/printer.rb
124
+ - lib/partial_finder/railtie.rb
125
+ - lib/partial_finder/router.rb
126
+ - lib/partial_finder/runner.rb
127
+ - lib/partial_finder/version.rb
128
+ - lib/tasks/debug.rake
129
+ - lib/tasks/find.rake
130
+ - lib/tasks/help.rake
131
+ - partial_finder.gemspec
132
+ homepage: https://github.com/Negotiatus/Partial-Finder
133
+ licenses:
134
+ - MIT
135
+ metadata:
136
+ homepage_uri: https://github.com/Negotiatus/Partial-Finder
137
+ source_code_uri: https://github.com/Negotiatus/Partial-Finder
138
+ changelog_uri: https://github.com/Negotiatus/Partial-Finder
139
+ post_install_message:
140
+ rdoc_options: []
141
+ require_paths:
142
+ - lib
143
+ required_ruby_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ required_rubygems_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ requirements: []
154
+ rubygems_version: 3.0.3.1
155
+ signing_key:
156
+ specification_version: 4
157
+ summary: Finds app routes and render paths given a partial file name.
158
+ test_files: []