refactor 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 237c531a5ad2b86c4ef7cf3276e448f23fb26a5f
4
- data.tar.gz: 6299ca36877ccbaa5e7b39ec8c6045defb3c06c7
2
+ SHA256:
3
+ metadata.gz: 4a5e69664afcb410d257cc1cf07419ad948dbf36b52a8530c11901985561bd99
4
+ data.tar.gz: 4c96cfe97ef59000be90ecbfb071e613e8f9c7081d8efc20e47344f149c45a86
5
5
  SHA512:
6
- metadata.gz: 871c020fcbccbe84385e59fb5276509a9d157ae77a39ffdc0991b4826aa039bc1a351edabe0122743c55fc5e152c92ed80e4f89d1d2abf5d87698cb0ea1ccde9
7
- data.tar.gz: d163e31206b0fd386b6baaab416be47bc32cf9c118e20ff8b87d689501dac8b72d0c6383017179e7ee1d32bc93ac90c07dab594ef2e328193aecf8e613f9164b
6
+ metadata.gz: f22be527ca6164f7b1997f4d1de1a40c835f4e3664a42bd02e2e019646374d7b70f5fdf3815e535f3b73e026f2c05f4d1e9e8592633214f13a15bdfe02b99afd
7
+ data.tar.gz: 1e1b728e24065868e8a821de6f39e9d033fb3a2611fd1a2c10e8917d83601b08d8823fab66c8f0dd9f98b5e6569d335bae7132ccb4344b3ea24a1dccbbe609d8
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --format documentation
2
2
  --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,6 @@
1
+ Metrics/BlockLength:
2
+ Enabled: false
3
+ Style/StringLiterals:
4
+ Enabled: false
5
+ Style/AccessModifierDeclarations:
6
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-3.3.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-02-27
4
+
5
+ - Gem transferred to @baweaver
6
+
7
+ ## [0.0.1] - 2014-06-12
8
+
9
+ - Initial release by original author: https://github.com/afeld/refactor
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at keystonelemur@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/Guardfile ADDED
@@ -0,0 +1,17 @@
1
+ guard :rspec, cmd: "bundle exec rspec" do
2
+ require "guard/rspec/dsl"
3
+
4
+ dsl = Guard::RSpec::Dsl.new(self)
5
+
6
+ # RSpec files
7
+ rspec = dsl.rspec
8
+
9
+ watch(rspec.spec_helper) { rspec.spec_dir }
10
+ watch(rspec.spec_support) { rspec.spec_dir }
11
+ watch(rspec.spec_files)
12
+
13
+ # Ruby files
14
+ ruby = dsl.ruby
15
+
16
+ dsl.watch_spec_files_for(ruby.lib_files)
17
+ end
data/LICENSE.txt CHANGED
@@ -1,22 +1,21 @@
1
- Copyright (c) 2014 Aidan Feldman
1
+ The MIT License (MIT)
2
2
 
3
- MIT License
3
+ Copyright (c) 2024 Brandon Weaver
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
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:
12
11
 
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
15
14
 
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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 CHANGED
@@ -1,15 +1,114 @@
1
1
  # Refactor
2
2
 
3
- A command line tool to help refactor your code.
3
+ Utilities for refactoring and upgrading Ruby code based on ASTs.
4
+
5
+ Consider reading [ASTs in Ruby - Pattern Matching](https://dev.to/baweaver/asts-in-ruby-pattern-matching-mjd) as a primer for using this gem, as it will introduce concepts of pattern matching and ASTs in more detail.
6
+
7
+ ## Original Attribution
8
+
9
+ This gem is a continuation of the work by @afeld and their Refactor gem here:
10
+
11
+ https://github.com/afeld/refactor
4
12
 
5
13
  ## Usage
6
14
 
7
- Make sure your code is backed up (e.g. under version control and fully committed) first!
15
+ Refactor works via rules, similar to RuboCop:
16
+
17
+ ```ruby
18
+ # Inherits from base rule, which provides a lot of utilities
19
+ # to match and replace with.
20
+ class ShorthandRule < Refactor::Rule
21
+ # The code we're trying to work with here is:
22
+ #
23
+ # [1, 2, 3].select { |v| v.even? }
24
+ #
25
+ # ...and we want to make it into:
26
+ #
27
+ # [1, 2, 3].select(&:even?)
28
+ #
29
+ def on_block(node)
30
+ return unless node in [:block, receiver,
31
+ [[:arg, arg_name]], [:send, [:lvar, ^arg_name], method_name]
32
+ ]
33
+
34
+ replace(node, "#{receiver.source}(&:#{method_name})")
35
+ end
36
+ end
37
+
38
+ ShorthandRule.process("[1, 2, 3].select { |v| v.even? }")
39
+ # => [1, 2, 3].select(&:even?)
40
+ ```
41
+
42
+ If we add multiple rules we'll want to use a `Rewriter` instead to apply all of them without the potential for collision:
43
+
44
+ ```ruby
45
+ class BigDecimalRule < Refactor::Rule
46
+ def on_send(node)
47
+ return unless node in [:send, _, :BigDecimal,
48
+ [:float | :int, value]
49
+ ]
50
+
51
+ replace(node, "BigDecimal('#{value}')")
52
+ end
53
+ end
54
+
55
+ class HashRefDefaultRule < Refactor::Rule
56
+ def on_send(node)
57
+ return unless node in [:send, [:const, nil, :Hash], :new,
58
+ [:array | :hash] => reference_value
59
+ ]
60
+
61
+ replace(node, "Hash.new { |h, k| h[k] = #{reference_value.source} }")
62
+ end
63
+ end
64
+
65
+ rewriter = Refactor::Rewriter.new(rules: [ShorthandRule, BigDecimalRule, HashRefDefaultRule])
66
+ rewriter.process <<~RUBY
67
+ [1, 2, 3].select { |v| v.even? }
68
+
69
+ value = BigDecimal(5.3)
70
+ groups = Hash.new({})
71
+ RUBY
72
+ # => <<~RUBY
73
+ # [1, 2, 3].select(&:even?)
74
+ #
75
+ # value = BigDecimal('5.3')
76
+ # groups = Hash.new { |h, k| h[k] = {} }
77
+ # RUBY
78
+ ```
79
+
80
+ ## Why Not RuboCop?
8
81
 
9
- ```bash
82
+ In most cases you likely want to use RuboCop as it has more robust support and testing. This gem is currently more experimental and focused exclusively on refactoring and AST manipulations in a more minimal sense.
83
+
84
+ ## Installation
85
+
86
+ Install the gem and add to the application's Gemfile by executing:
87
+
88
+ ```
89
+ bundle add refactor
90
+ ```
91
+
92
+ If bundler is not being used to manage dependencies, install the gem by executing:
93
+
94
+ ```
10
95
  gem install refactor
11
- # Then, from your project (sub)directory:
12
- refactor FROM TO
13
96
  ```
14
97
 
15
- where `FROM` and `TO` can each be a name that `has_underscores`, is `CamelCased`, `has-dashes`, or is `"separated with spaces between quotes"`. It will replace the `FROM` with the `TO` in the corresponding format.
98
+ ## Development
99
+
100
+ 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.
101
+
102
+ 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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
103
+
104
+ ## Contributing
105
+
106
+ Bug reports and pull requests are welcome on GitHub at https://github.com/baweaver/refactor. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/baweaver/refactor/blob/main/CODE_OF_CONDUCT.md).
107
+
108
+ ## License
109
+
110
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
111
+
112
+ ## Code of Conduct
113
+
114
+ Everyone interacting in the Refactor project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/baweaver/refactor/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rspec/core/rake_task"
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec)
5
7
 
6
- task :default => :spec
7
-
8
+ task default: :spec
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Refactor
2
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
3
5
  end
data/lib/refactor.rb CHANGED
@@ -1,68 +1,86 @@
1
- require 'active_support/core_ext/string/inflections'
2
- require 'fileutils'
1
+ # frozen_string_literal: true
3
2
 
4
3
  require_relative 'refactor/version'
5
4
 
5
+ # Eventually we may consider dropping RuboCop and working more directly
6
+ # on top of Parser itself
7
+ require 'rubocop'
8
+
6
9
  module Refactor
7
- def run(from, to)
8
- from_camelized = from.camelize
9
- from_dashed = from.dasherize
10
- from_humanized = from.humanize
11
- from_underscored = from.underscore
12
- to_camelized = to.camelize
13
- to_dashed = to.dasherize
14
- to_underscored = to.underscore
15
-
16
- camelized_regex = /(?<=\b|_)#{Regexp.quote(from_camelized)}(?=\b|_)/
17
- dashed_regex = /(?<=\b|_)#{Regexp.quote(from_dashed)}(?=\b|_)/
18
- underscored_regex = /(?<=\b|_)#{Regexp.quote(from_underscored)}(?=\b|_)/
19
-
20
- # all files in current directory
21
- Dir.glob('**/*') do |old_path|
22
- # ignore certain directories
23
- unless old_path =~ %r{\A(coverage|pkg|tmp|vendor)(\z|/)}
24
- # only check the basename so that the directory doesn't get renamed twice
25
- old_basename = File.basename(old_path)
26
- new_basename = old_basename.dup
27
- new_basename.gsub!(dashed_regex, to_dashed)
28
- new_basename.gsub!(underscored_regex, to_underscored)
29
-
30
- if new_basename == old_basename
31
- # no change
32
- new_path = old_path
33
- else
34
- # rename file
35
- new_path = File.join(File.dirname(old_path), new_basename)
36
- puts "#{old_path} –> #{new_path}"
37
- FileUtils.mv(old_path, new_path)
38
- end
39
-
40
- if File.file?(new_path)
41
- # replace within file
42
- old_text = File.read(new_path)
43
-
44
- new_text = old_text.dup
45
- new_text.gsub!(camelized_regex, to_camelized)
46
- new_text.gsub!(dashed_regex, to_dashed)
47
- new_text.gsub!(underscored_regex, to_underscored)
48
-
49
- unless new_text == old_text
50
- # rewrite existing file
51
- File.write(new_path, new_text)
52
- end
53
-
54
- # show possible matches in body
55
- line_num = 0
56
- new_text.each_line do |old_line|
57
- new_line = old_line.gsub(/#{Regexp.quote(from_humanized)}/i, "\e[33m\\0\e[0m")
58
- unless new_line == old_line
59
- puts "\e[36m#{new_path}:#{line_num}\e[0m #{new_line}"
60
- end
61
- line_num += 1
62
- end
63
- end
10
+ # Utilities for working with ASTs
11
+ module Util
12
+ def self.deconstruct_ast(string)
13
+ deep_deconstruct(ast_from(string))
14
+ end
15
+
16
+ # Makes it easier to break down an AST into what we'd like to match against
17
+ # eventually.
18
+ def self.deep_deconstruct(node)
19
+ return node unless node.respond_to?(:deconstruct)
20
+
21
+ node.deconstruct.map { deep_deconstruct(_1) }
22
+ end
23
+
24
+ # Convert a string into its AST representation
25
+ def self.ast_from(string)
26
+ processed_source_from(string).ast
27
+ end
28
+
29
+ def self.processed_source_from(string)
30
+ RuboCop::ProcessedSource.new(string, RUBY_VERSION.to_f)
31
+ end
32
+ end
33
+
34
+ # Wrapper for rule processors to simplify the code
35
+ # needed to run one.
36
+ class Rule < Parser::AST::Processor
37
+ include RuboCop::AST::Traversal
38
+
39
+ protected attr_reader :rewriter
40
+
41
+ def initialize(rewriter)
42
+ @rewriter = rewriter
43
+ super()
44
+ end
45
+
46
+ def self.process(string)
47
+ Rewriter.new(rules: [self]).process(string)
48
+ end
49
+
50
+ def process_regular_node(node)
51
+ return matches(node) if defined?(matches)
52
+
53
+ super()
54
+ end
55
+
56
+ protected def replace(node, new_code)
57
+ rewriter.replace(node.loc.expression, new_code)
58
+ end
59
+ end
60
+
61
+ # Full rewriter, typically used for processing multiple rules
62
+ class Rewriter
63
+ def initialize(rules: [])
64
+ @rules = rules
65
+ end
66
+
67
+ def process(string)
68
+ # No sense in processing anything if there's nothing to apply it to
69
+ return string if @rules.empty?
70
+
71
+ source = Util.processed_source_from(string)
72
+ ast = source.ast
73
+
74
+ source_buffer = source.buffer
75
+
76
+ rewriter = Parser::Source::TreeRewriter.new(source_buffer)
77
+
78
+ @rules.each do |rule_class|
79
+ rule = rule_class.new(rewriter)
80
+ ast.each_node { |node| rule.process(node) }
64
81
  end
82
+
83
+ rewriter.process
65
84
  end
66
85
  end
67
- module_function :run
68
86
  end
data/refactor.gemspec CHANGED
@@ -1,26 +1,37 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'refactor/version'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/refactor/version"
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "refactor"
8
- spec.version = Refactor::VERSION
9
- spec.authors = ["Aidan Feldman"]
10
- spec.email = ["aidan.feldman@gmail.com"]
11
- spec.summary = %q{A command line tool to help refactor your code.}
12
- # spec.description = %q{TODO: Write a longer description. Optional.}
13
- spec.homepage = "https://github.com/afeld/refactor"
14
- spec.license = "MIT"
6
+ spec.name = "refactor"
7
+ spec.version = Refactor::VERSION
8
+ spec.authors = ["Brandon Weaver"]
9
+ spec.email = ["keystonelemur@gmail.com"]
10
+
11
+ spec.summary = "Ruby refactoring tool"
12
+ spec.description = "AST-based Ruby refactoring toolkit"
13
+ spec.homepage = "https://www.github.com/baweaver/refactor"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 3.1.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
15
20
 
16
- spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (File.expand_path(f) == __FILE__) ||
26
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
27
+ end
28
+ end
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
19
31
  spec.require_paths = ["lib"]
20
32
 
21
- spec.add_dependency "activesupport", "~> 4.0"
33
+ # Consider removing this later and having a more minimal subset
34
+ spec.add_dependency "rubocop"
22
35
 
23
- spec.add_development_dependency "bundler", "~> 1.6"
24
- spec.add_development_dependency "rake"
25
- spec.add_development_dependency "rspec"
36
+ spec.add_development_dependency "guard-rspec"
26
37
  end
@@ -0,0 +1,288 @@
1
+ # All the stuff I've extracted for the moment until I get it working cleanly
2
+ # or for release in a later commit. For now this may be interesting for the
3
+ # sake of reading so I'll leave this in.
4
+ module Refactor
5
+ module Util
6
+ REPL_SOURCE = Regexp.union('(IRB)', '(pry)')
7
+
8
+ def self.extract_block_ast(block)
9
+ source_file, source_line = block.source_location
10
+
11
+ # REPL defined, we gotta do some more work here
12
+
13
+ raise ArgumentError, 'Cannot extract source from REPL' if REPL_SOURCE.match?(source_file)
14
+
15
+ file_ast = ast_from(File.read(source_file))
16
+
17
+ file_ast.each_node.find do |node|
18
+ node.block_type? && node.first_line == source_line
19
+ end
20
+ end
21
+
22
+ def self.pattern_deconstruct(node, tokens: [], seen: Hash.new(0), is_head: false)
23
+ if node.respond_to?(:deconstruct)
24
+ # Receivers especially tend to get nested like this
25
+ if node in [:send, nil, potential_token]
26
+ if potential_token == :_ || tokens.include?(potential_token)
27
+ return Tokens::Literal.new(potential_token)
28
+ end
29
+ end
30
+
31
+ node.deconstruct.each_with_index.map do |node, i|
32
+ pattern_deconstruct(node, tokens:, seen:, is_head: i.zero?)
33
+ end
34
+ else
35
+ return node if node.nil? || is_head || !tokens.include?(node)
36
+
37
+ seen[node] += 1
38
+ is_repeated = seen[node] > 1
39
+
40
+ Tokens::Literal.new("#{'^' if is_repeated}#{node}")
41
+ end
42
+ end
43
+ end
44
+
45
+ # Any special tokens that do not cleanly introspect.
46
+ module Tokens
47
+ # Prevents quotes from displaying when assembling
48
+ # code later.
49
+ class Literal
50
+ def initialize(string)
51
+ @string = string
52
+ end
53
+
54
+ def to_s
55
+ @string
56
+ end
57
+
58
+ def inspect
59
+ @string
60
+ end
61
+ end
62
+ end
63
+
64
+ # Wrap a pattern match, potentially do other interesting things
65
+ # later on.
66
+ class Matcher
67
+ def initialize(&pattern)
68
+ @pattern = pattern
69
+ end
70
+
71
+ def call(value)
72
+ @pattern.call(value)
73
+ end
74
+
75
+ alias === call
76
+
77
+ def to_proc
78
+ @pattern
79
+ end
80
+ end
81
+
82
+ # Not sure this is a wise idea, but it _is_ a fun idea.
83
+ module RuleMacros
84
+ def matches(string = nil, &block)
85
+ return matches_block(&block) if block_given?
86
+
87
+ # Sort is a stupid hack to beat partial word matches. Should make this smarter
88
+ # later on. Probably invoke this into a tree rewriter ruleset which
89
+ # is far more involved than I want to do tonight
90
+ tokens = string.scan(/\$\w+/).sort_by { -_1.size }
91
+ token_match = Regexp.union(tokens)
92
+ clean_string = string.gsub(token_match) { |v| v[1..-1] }
93
+ input_ast = ast_from(clean_string)
94
+ pattern_type = input_ast.type
95
+
96
+ # Strip off the global-psuedo-var syntax
97
+ scanned_tokens = tokens.map { _1[1..-1].to_sym }
98
+ match_data_hash = scanned_tokens
99
+ .map { |v| "#{v}:" }
100
+ .then { |vs| "{ #{vs.join(', ')}, pattern_type: :#{pattern_type} }" }
101
+
102
+ pattern_match_stanza = Util.pattern_deconstruct(input_ast, tokens: scanned_tokens)
103
+
104
+ source = <<~RUBY
105
+ def pattern_type
106
+ :#{pattern_type}
107
+ end
108
+
109
+ def match?(value)
110
+ ast = value.is_a?(RuboCop::AST::Node) ? value : Util.ast_from(value)
111
+
112
+ return false unless ast in #{pattern_match_stanza}
113
+ return false unless ast.type == :#{pattern_type}
114
+
115
+ true
116
+ end
117
+
118
+ def match(value)
119
+ ast = value.is_a?(RuboCop::AST::Node) ? value : Util.ast_from(value)
120
+
121
+ return false unless ast in #{pattern_match_stanza}
122
+ return false unless ast.type == :#{pattern_type}
123
+
124
+ #{match_data_hash}
125
+ end
126
+ RUBY
127
+
128
+ instance_eval(source)
129
+ end
130
+
131
+ def replace(string = nil, &block)
132
+ block_ast = Util.extract_block_ast(block)
133
+
134
+ # pp block_ast
135
+ # pp Util.deep_deconstruct(block_ast)
136
+
137
+ does_match = block_ast in [:block,
138
+ [:send, nil, :replace],
139
+ [[:arg, node_arg_name], [:arg, match_data_arg_name]],
140
+ replacement_body
141
+ ]
142
+
143
+ unless does_match
144
+ raise ArgumentError, "Block is not in correct format for matches macro: \n#{block_ast.source}"
145
+ end
146
+
147
+ new_method_source = <<~RUBY
148
+ def translate(#{node_arg_name}, #{match_data_arg_name})
149
+ #{replacement_body.source}
150
+ end
151
+
152
+ def match_and_replace(node)
153
+ pp(node:)
154
+ match_data = matches(node) or return false
155
+ pp(match_data:)
156
+ new_source = translate(node, match_data)
157
+ pp(new_source:)
158
+
159
+ @rewriter.replace(node.loc.expression, new_source)
160
+ end
161
+ RUBY
162
+
163
+ puts '', "REPLACE", new_method_source, '', ''
164
+
165
+ instance_eval(new_method_source)
166
+ end
167
+
168
+ private def matches_block(&block)
169
+ block_ast = Util.extract_block_ast(block)
170
+
171
+ does_match = block_ast in [:block,
172
+ [:send, nil, :matches],
173
+ [[:arg, node_arg_name]],
174
+ [:match_pattern_p,
175
+ [:lvar, ^node_arg_name], # Should be the same, or it'd fail anyways
176
+ [:array_pattern, [:sym, pattern_type], *], # The rest doesn't really matter
177
+ *
178
+ ] => pattern_match_stanza
179
+ ]
180
+
181
+ unless does_match
182
+ raise ArgumentError, "Block is not in correct format for matches macro: \n#{block_ast.source}"
183
+ end
184
+
185
+ # Requires descent, re-iterate
186
+ match_variables = block_ast.each_node.select { |n| n.type == :match_var }.map(&:source)
187
+ match_data_hash = match_variables
188
+ .map { |v| "#{v}:" }
189
+ .then { |vs| "{ #{vs.join(', ')}, pattern_type: :#{pattern_type} }" }
190
+
191
+ new_method_source = <<~RUBY
192
+ def pattern_type
193
+ :#{pattern_type}
194
+ end
195
+
196
+ def on_#{pattern_type}(node)
197
+ pp(on_#{pattern_type}: true, node:, pattern_type:)
198
+ match_and_replace(node)
199
+ end
200
+
201
+ def match_and_replace(node)
202
+ return # Fake version until a replacement happens and overwrites this later
203
+ end
204
+
205
+ def match?(#{node_arg_name})
206
+ return false unless #{node_arg_name}.type == :#{pattern_type}
207
+ return false unless #{pattern_match_stanza.source}
208
+
209
+ true
210
+ end
211
+
212
+ def match(#{node_arg_name})
213
+ return false unless #{node_arg_name}.type == :#{pattern_type}
214
+ return false unless #{pattern_match_stanza.source}
215
+
216
+ #{match_data_hash}
217
+ end
218
+ RUBY
219
+
220
+ puts '', "MATCH", new_method_source, '', ''
221
+
222
+ instance_eval(new_method_source)
223
+ end
224
+
225
+ private def create_translation_function(input_string:, target_string:)
226
+ # Sort is a stupid hack to beat partial word matches. Should make this smarter
227
+ # later on. Probably invoke this into a tree rewriter ruleset which
228
+ # is far more involved than I want to do tonight
229
+ input_tokens = input_string.scan(/\$\w+/).sort_by { -_1.size }
230
+ input_token_match = Regexp.union(input_tokens)
231
+ input_clean = input_string.gsub(input_token_match) { |v| v[1..-1] }
232
+ input_ast = ast_from(input_clean)
233
+
234
+ # Strip off the global-psuedo-var syntax
235
+ input_scan_tokens = input_tokens.map { _1[1..-1].to_sym }
236
+
237
+ pattern_match_stanza = pattern_deconstruct(input_ast, tokens: input_scan_tokens)
238
+
239
+ # Interpolate the new values
240
+ #
241
+ # Don't mind the really danged hacky AST to source coercion here,
242
+ # need to think on cleaning that up real fast later.
243
+ target_source = target_string.gsub(input_token_match) do |v|
244
+ "\#\{#{v[1..-1]}.then { |x| x.is_a?(RuboCop::AST::Node) ? x.source : x }\}"
245
+ end
246
+
247
+ extractor_source = <<~RUBY
248
+ -> node do
249
+ node = node.is_a?(String) ? ast_from(node) : node
250
+ return unless node in #{pattern_match_stanza}
251
+
252
+ "#{target_source}"
253
+ end
254
+ RUBY
255
+
256
+ puts extractor_source
257
+
258
+ eval(extractor_source)
259
+ end
260
+ end
261
+ end
262
+
263
+ # Tests for RuleMacros for later
264
+ context 'When using RuleMacros' do
265
+ let(:macro_rule) do
266
+ Class.new(Refactor::Rule) do
267
+ matches do |macro_node|
268
+ macro_node in [:block, receiver,
269
+ [[:arg, arg_name]], [:send, [:lvar, ^arg_name], method_name]
270
+ ]
271
+ end
272
+
273
+ replace do |_macro_node, match_data|
274
+ "#{match_data[:receiver].source}(&:#{match_data[:method_name]})"
275
+ end
276
+ end
277
+ end
278
+
279
+ it 'creates a valid rule' do
280
+ expect(macro_rule.superclass).to eq(Refactor::Rule)
281
+ end
282
+
283
+ describe ".process" do
284
+ it "processes a rule inline for convenience" do
285
+ expect(macro_rule.process(target_source)).to eq(corrected_source)
286
+ end
287
+ end
288
+ end
data/sig/refactor.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Refactor
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata CHANGED
@@ -1,116 +1,88 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: refactor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
- - Aidan Feldman
8
- autorequire:
9
- bindir: bin
7
+ - Brandon Weaver
8
+ autorequire:
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2014-06-12 00:00:00.000000000 Z
11
+ date: 2024-05-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: activesupport
14
+ name: rubocop
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
18
- - !ruby/object:Gem::Version
19
- version: '4.0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ~>
25
- - !ruby/object:Gem::Version
26
- version: '4.0'
27
- - !ruby/object:Gem::Dependency
28
- name: bundler
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ~>
32
- - !ruby/object:Gem::Version
33
- version: '1.6'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ~>
39
- - !ruby/object:Gem::Version
40
- version: '1.6'
41
- - !ruby/object:Gem::Dependency
42
- name: rake
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - '>='
17
+ - - ">="
46
18
  - !ruby/object:Gem::Version
47
19
  version: '0'
48
- type: :development
20
+ type: :runtime
49
21
  prerelease: false
50
22
  version_requirements: !ruby/object:Gem::Requirement
51
23
  requirements:
52
- - - '>='
24
+ - - ">="
53
25
  - !ruby/object:Gem::Version
54
26
  version: '0'
55
27
  - !ruby/object:Gem::Dependency
56
- name: rspec
28
+ name: guard-rspec
57
29
  requirement: !ruby/object:Gem::Requirement
58
30
  requirements:
59
- - - '>='
31
+ - - ">="
60
32
  - !ruby/object:Gem::Version
61
33
  version: '0'
62
34
  type: :development
63
35
  prerelease: false
64
36
  version_requirements: !ruby/object:Gem::Requirement
65
37
  requirements:
66
- - - '>='
38
+ - - ">="
67
39
  - !ruby/object:Gem::Version
68
40
  version: '0'
69
- description:
41
+ description: AST-based Ruby refactoring toolkit
70
42
  email:
71
- - aidan.feldman@gmail.com
72
- executables:
73
- - refactor
43
+ - keystonelemur@gmail.com
44
+ executables: []
74
45
  extensions: []
75
46
  extra_rdoc_files: []
76
47
  files:
77
- - .gitignore
78
- - .rspec
79
- - .travis.yml
80
- - Gemfile
48
+ - ".rspec"
49
+ - ".rubocop.yml"
50
+ - ".ruby-version"
51
+ - CHANGELOG.md
52
+ - CODE_OF_CONDUCT.md
53
+ - Guardfile
81
54
  - LICENSE.txt
82
55
  - README.md
83
56
  - Rakefile
84
- - bin/refactor
85
57
  - lib/refactor.rb
86
58
  - lib/refactor/version.rb
87
59
  - refactor.gemspec
88
- - spec/refactor_spec.rb
89
- - spec/spec_helper.rb
90
- homepage: https://github.com/afeld/refactor
60
+ - sandbox/experimental.rb
61
+ - sig/refactor.rbs
62
+ homepage: https://www.github.com/baweaver/refactor
91
63
  licenses:
92
64
  - MIT
93
- metadata: {}
94
- post_install_message:
65
+ metadata:
66
+ homepage_uri: https://www.github.com/baweaver/refactor
67
+ source_code_uri: https://www.github.com/baweaver/refactor
68
+ changelog_uri: https://www.github.com/baweaver/refactor/CHANGELOG.md
69
+ post_install_message:
95
70
  rdoc_options: []
96
71
  require_paths:
97
72
  - lib
98
73
  required_ruby_version: !ruby/object:Gem::Requirement
99
74
  requirements:
100
- - - '>='
75
+ - - ">="
101
76
  - !ruby/object:Gem::Version
102
- version: '0'
77
+ version: 3.1.0
103
78
  required_rubygems_version: !ruby/object:Gem::Requirement
104
79
  requirements:
105
- - - '>='
80
+ - - ">="
106
81
  - !ruby/object:Gem::Version
107
82
  version: '0'
108
83
  requirements: []
109
- rubyforge_project:
110
- rubygems_version: 2.2.2
111
- signing_key:
84
+ rubygems_version: 3.5.3
85
+ signing_key:
112
86
  specification_version: 4
113
- summary: A command line tool to help refactor your code.
114
- test_files:
115
- - spec/refactor_spec.rb
116
- - spec/spec_helper.rb
87
+ summary: Ruby refactoring tool
88
+ test_files: []
data/.gitignore DELETED
@@ -1,22 +0,0 @@
1
- *.gem
2
- *.rbc
3
- .bundle
4
- .config
5
- .yardoc
6
- Gemfile.lock
7
- InstalledFiles
8
- _yardoc
9
- coverage
10
- doc/
11
- lib/bundler/man
12
- pkg
13
- rdoc
14
- spec/reports
15
- test/tmp
16
- test/version_tmp
17
- tmp
18
- *.bundle
19
- *.so
20
- *.o
21
- *.a
22
- mkmf.log
data/.travis.yml DELETED
@@ -1,3 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.0.0
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in refactor.gemspec
4
- gemspec
data/bin/refactor DELETED
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require_relative File.join('..', 'lib', 'refactor')
4
-
5
- from = ARGV[0] || raise("Needs a FROM argument.")
6
- to = ARGV[1] || raise("Needs a TO argument.")
7
- Refactor.run(from, to)
@@ -1,11 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Refactor do
4
- it 'has a version number' do
5
- expect(Refactor::VERSION).not_to be nil
6
- end
7
-
8
- it 'does something useful' do
9
- expect(false).to eq(true)
10
- end
11
- end
data/spec/spec_helper.rb DELETED
@@ -1,2 +0,0 @@
1
- $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
- require 'refactor'