cease 0.1.1

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: cdd44d4a984dd4c0f68b3e3708dcf8e1e7d5ab85fda130a67a41fa5942473231
4
+ data.tar.gz: 0ca8399c3a97ad5de4c278c9d318556074a23ab5576b652d4d312f2197a4a008
5
+ SHA512:
6
+ metadata.gz: 87d2e9b44d400ec3696ccd509dfd7a55c6c4a47b6b1fb7635e9eedc6e76888aa62e51a6644324e686f920a16a7b454edc814fa3db6fca4676bb1a698ebc7c9e3
7
+ data.tar.gz: 325ce0face6fd0751041e911ec7e3d12ce8a5960a461cc85b5d55fe1463926141e252274cf99349b045c3d2d04641b9d1111f3906991f715c50cb830ed1df783
@@ -0,0 +1,25 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+
13
+ strategy:
14
+ matrix:
15
+ ruby: [2.6, 2.7, "3.0", "3.1", jruby-9.3]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v2
19
+ - name: Setup Ruby
20
+ uses: ruby/setup-ruby@v1
21
+ with:
22
+ ruby-version: ${{ matrix.ruby }}
23
+ bundler-cache: true
24
+ - name: Run specs
25
+ run: bundle exec rspec
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ Gemfile.lock
11
+ Gemfile.gpg
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -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 sung@dustybit.software. 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://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem "rake", "~> 12.0"
7
+ gem "minitest", "~> 5.0"
8
+ gem "rspec", "~> 3.5.0"
9
+ end
10
+
11
+ group :development, :test do
12
+ gem 'pry', '~> 0.13.1'
13
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2021 Dusty Bit Software, Inc.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ * Neither the name Solidus nor the names of its contributors may be used to
13
+ endorse or promote products derived from this software without specific
14
+ prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
20
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
21
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
22
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # Cease
2
+
3
+ ![GitHub Actions](https://github.com/DustyBitSoftware/cease/actions/workflows/ci.yml/badge.svg)
4
+
5
+ `cease` is a tool that scans for Ruby code marked as EOL.
6
+
7
+ ## Installation
8
+
9
+ Install via rubygems:
10
+ ```sh
11
+ gem install cease
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ Run it:
17
+ ```sh
18
+ cease [directory_or_source_file]*
19
+ ```
20
+
21
+ ## Example
22
+ ### Basic usage
23
+ Given a source file called `example.rb` that contains the following code:
24
+
25
+ ```ruby
26
+ # [cease] at 12pm on 1/1/1999
27
+ class RemoveMeLater
28
+ def foo
29
+ puts 'bar'
30
+ end
31
+ end
32
+ # [/cease]
33
+ ```
34
+
35
+ You should see the output:
36
+ ```sh
37
+ $ cease example.rb
38
+ Scanning 1 source(s)...
39
+
40
+ (example.rb)
41
+
42
+ [3, 9]: Overdue by roughly 23 years
43
+ class RemoveMeLater
44
+ def foo
45
+ puts 'bar'
46
+ end
47
+ end
48
+
49
+
50
+ Total of 1 evictions(s) found.
51
+ ```
52
+
53
+ ### Options
54
+ Cease supports both 12 and 24 hour clocks:
55
+ ```ruby
56
+ # [cease] at 13:00 on 1/1/1999
57
+ class RemoveMeLater
58
+ ...
59
+ # [/cease]
60
+ ```
61
+
62
+ Multiple commands per source:
63
+ ```ruby
64
+ # [cease] at 12pm on 1/1/1999
65
+ class RemoveMeLater
66
+ def foo
67
+ puts 'bar'
68
+ end
69
+ end
70
+ # [/cease]
71
+
72
+ # [cease] at 1pm on 3/3/3333
73
+ class RemoveMeWayLater
74
+ def foo
75
+ puts 'bar'
76
+ end
77
+ end
78
+ # [/cease]
79
+ ```
80
+
81
+ If a date isn't provided, Cease attempts to guess the date based on the git commit:
82
+ ```ruby
83
+ # [cease] at 13:00 # The date will be based on the commit timestamp of this comment.
84
+ class RemoveMeLater
85
+ ...
86
+ # [/cease]
87
+ ```
88
+
89
+ You can provide an optional timezone (defaults to UTC):
90
+ ```ruby
91
+ # [cease] at 1pm on 1/1/1999 { timezone: 'PST' }
92
+ class RemoveMeLater
93
+ ...
94
+ # [/cease]
95
+ ```
96
+
97
+ **NOTE: Do not nest commands! This will not work:**
98
+ ```ruby
99
+ # [cease] at 1pm
100
+ class RemoveMe
101
+ # [cease] at 3pm
102
+ def initialize
103
+ end
104
+ # [/cease]
105
+ end
106
+ # [/cease]
107
+ ```
108
+
109
+ ## Development
110
+
111
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
112
+ `rake test` to run the tests. You can also run `bin/console` for an interactive
113
+ prompt that will allow you to experiment.
114
+
115
+ To install this gem onto your local machine, run `bundle exec rake install`. To
116
+ release a new version, update the version number in `version.rb`, and then run
117
+ `bundle exec rake release`, which will create a git tag for the version, push
118
+ git commits and tags, and push the `.gem` file to
119
+ [rubygems.org](https://rubygems.org).
120
+
121
+ ## Contributing
122
+
123
+ Bug reports and pull requests are welcome on GitHub at
124
+ https://github.com/DustyBitSoftware/cease. This project is intended to be a safe,
125
+ welcoming space for collaboration, and contributors are expected to adhere to
126
+ the [code of
127
+ conduct](https://github.com/nohmar/cease/blob/master/CODE_OF_CONDUCT.md).
128
+
129
+
130
+ ## Code of Conduct
131
+
132
+ Everyone interacting in the Cease project's codebases, issue trackers, chat
133
+ rooms and mailing lists is expected to follow the [code of
134
+ conduct](https://github.com/nohmar/cease/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/cease ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/cease'
4
+
5
+ exit Cease::CLI.new(argv: ARGV).execute
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "cease"
5
+ require "pry"
6
+
7
+ Pry.start
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/cease.gemspec ADDED
@@ -0,0 +1,34 @@
1
+ require_relative 'lib/cease/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "cease"
5
+ spec.version = Cease::VERSION
6
+ spec.authors = ["Sung Noh"]
7
+ spec.email = ["sung@dustybit.software"]
8
+
9
+ spec.summary = 'Evict unused code at some time in the future'
10
+ spec.description = 'Cease is a tool that detects blocks of code ' \
11
+ 'to be removed at a specified time.'
12
+ spec.homepage = 'https://github.com/DustyBitSoftware/cease'
13
+ spec.license = 'BSD-3-Clause'
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = spec.homepage
18
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+ spec.bindir = "bin"
26
+ spec.executables = spec.files.grep(%r{^bin/}).map { |path| File.basename(path) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.add_runtime_dependency 'parser', '~> 3.0.0'
30
+ spec.add_runtime_dependency 'git', '~> 1.10.0'
31
+ spec.add_runtime_dependency 'rainbow', '~> 3.0.0'
32
+ spec.add_runtime_dependency 'dotiw', '~> 5.3.2'
33
+ spec.add_runtime_dependency 'tzinfo', '~> 2.0.4'
34
+ end
data/lib/cease/cli.rb ADDED
@@ -0,0 +1,53 @@
1
+ require 'pathname'
2
+
3
+ require_relative 'report'
4
+
5
+ module Cease
6
+ class CLI
7
+ def initialize(argv: ARGV)
8
+ @argv = argv
9
+ end
10
+
11
+ def execute
12
+ report.execute
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :argv
18
+
19
+ def report
20
+ Report.new(sources: sources)
21
+ end
22
+
23
+ def sources
24
+ entries.each_with_object([]) do |given_path, paths|
25
+ next unless given_path.exist?
26
+ next if hidden_entry?(given_path)
27
+
28
+ relevant_paths = []
29
+
30
+ # Peform a depth-first search for a given Pathname.
31
+ given_path.find do |path|
32
+ relevant_paths << path if ruby_file?(path)
33
+ end
34
+
35
+ paths.concat(relevant_paths)
36
+ end
37
+ end
38
+
39
+ def entries
40
+ return Pathname.new('.').entries if argv.empty?
41
+ argv.map { |arg| Pathname.new(arg) }
42
+ end
43
+
44
+
45
+ def hidden_entry?(path)
46
+ path.basename.to_s.start_with? '.'
47
+ end
48
+
49
+ def ruby_file?(path)
50
+ path.extname == '.rb'
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,81 @@
1
+ require 'forwardable'
2
+
3
+ module Cease
4
+ module Eviction
5
+ class Chunk
6
+ extend Forwardable
7
+
8
+ # @params ast [Parser::AST::Node]
9
+ # @params statement [Cease::Eviction::Statement]
10
+ def initialize(ast:, statement:)
11
+ @ast = ast
12
+ @statement = statement
13
+ @closest_parent = nil
14
+ @children = []
15
+ end
16
+
17
+ # @return [Array<Parser::AST::Node>]
18
+ def extract
19
+ return ast if only_child?
20
+
21
+ find_closest_parent(ast)
22
+ find_children(closest_parent)
23
+ children
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :ast, :closest_parent, :statement, :children
29
+
30
+ delegate [:open_comment, :close_comment] => :statement
31
+
32
+ # @note A depth-first search to find the closest non-comment parent to
33
+ # the open comment. The closest parent is found outside the boundaries
34
+ # of the Eviction statement.
35
+ def find_closest_parent(ast)
36
+ @closest_parent = ast
37
+
38
+ return unless open_comment > ast
39
+
40
+ filter_children(ast) do |child|
41
+ # Skip the child if it begins past the closing comment.
42
+ next if close_comment < child
43
+
44
+ # Skip the child if the expression begins and ends on the same line
45
+ # e.g. send, array, symbol
46
+ next if open_comment > child && close_comment > child
47
+
48
+ find_closest_parent(child)
49
+ end
50
+ end
51
+
52
+ def find_children(ast)
53
+ return children unless ast
54
+
55
+ filter_children(ast) do |child|
56
+ if open_comment > child && !close_comment.nested_in?(child)
57
+ @children << child
58
+ next # We got what we need. Don't go digging for other children.
59
+ end
60
+
61
+ find_children(child)
62
+ end
63
+ end
64
+
65
+ def filter_children(ast)
66
+ return unless block_given?
67
+
68
+ ast.children.grep(Parser::AST::Node).each do |child|
69
+ next unless child.respond_to? :loc
70
+ next unless child.loc.expression
71
+
72
+ yield(child)
73
+ end
74
+ end
75
+
76
+ def only_child?
77
+ ast.children.count < 2
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,136 @@
1
+ require 'forwardable'
2
+ require 'yaml'
3
+
4
+ require_relative '../../git'
5
+
6
+ module Cease
7
+ module Eviction
8
+ module Command
9
+ class DateTime
10
+ extend Forwardable
11
+
12
+ class BadOptionsError < StandardError; end
13
+
14
+ DEFAULT_TIMEZONE = 'UTC'.freeze
15
+ TIMEZONE_NAME_MAPPING = {
16
+ "UTC" => "Etc/UTC",
17
+ "PST" => "America/Los_Angeles",
18
+ "MTZ" => "America/Denver",
19
+ "CST" => "America/Chicago",
20
+ "EST" => "America/New_York"
21
+ }.freeze
22
+ TIMEZONE_KEY = "timezone".freeze
23
+
24
+ # @params comment [Cease::Eviction::Comment]
25
+ # @params date_time [String]
26
+ # @params options [String]
27
+ def initialize(comment, date_time, options)
28
+ @comment = comment
29
+ @time, @date = parse(date_time)
30
+ @options = options
31
+ end
32
+
33
+ # @return [TZ::DateTimeWithOffset, nil]
34
+ def parsed_in_timezone
35
+ return unless valid?
36
+ tz.local_datetime(*local_datetime_args)
37
+ end
38
+
39
+ def valid?
40
+ return false unless valid_time?
41
+ return false if date && !valid_date?
42
+ return false if timezone_name && !valid_timezone?
43
+ true
44
+ rescue
45
+ false
46
+ end
47
+
48
+ def guess?
49
+ return true unless parsed_date
50
+ !!parsed_date
51
+ rescue
52
+ true
53
+ end
54
+
55
+ def tz
56
+ ::TZInfo::Timezone.get(
57
+ TIMEZONE_NAME_MAPPING[timezone_name] ||
58
+ TIMEZONE_NAME_MAPPING[DEFAULT_TIMEZONE]
59
+ )
60
+ end
61
+
62
+ private
63
+
64
+ delegate [:year, :month, :day, :hour, :minute] => :best_guess_date
65
+
66
+ attr_reader :comment, :options, :time, :date
67
+
68
+ def parse(date_time)
69
+ split = date_time.split(' ')
70
+ results = []
71
+
72
+ loop do
73
+ break if split.length == 0
74
+ results << split.shift(2)
75
+ end
76
+
77
+ results
78
+ .partition { |result| result.include?("at") }
79
+ .map { |result| _identifer, value = result.flatten; value }
80
+ end
81
+
82
+ def parsed_options
83
+ @parsed_options ||= YAML.safe_load(options || '')
84
+ rescue Psych::SyntaxError
85
+ raise BadOptionsError
86
+ end
87
+
88
+ def timezone_name
89
+ return unless parsed_options
90
+ parsed_options[TIMEZONE_KEY]
91
+ end
92
+
93
+ def valid_time?
94
+ return false unless time
95
+ parsed_time
96
+ end
97
+
98
+ def valid_date?
99
+ return false unless date
100
+ parsed_date
101
+ end
102
+
103
+ def valid_timezone?
104
+ return true if TIMEZONE_NAME_MAPPING.keys.include?(timezone_name)
105
+ false
106
+ end
107
+
108
+ def best_guess_date
109
+ @best_guess_date ||= begin
110
+ parsed_date ||
111
+ (comment.last_commit_date &&
112
+ ::DateTime.parse(comment.last_commit_date.to_s)) ||
113
+ ::DateTime.now
114
+ end
115
+ end
116
+
117
+ def local_datetime_args
118
+ [year, month, day, hour, minute]
119
+ end
120
+
121
+ def parsed_date
122
+ return unless date
123
+
124
+ ::DateTime.strptime(
125
+ "#{date} #{parsed_time.hour}:#{parsed_time.minute}",
126
+ "%m/%d/%Y %H:%M"
127
+ )
128
+ end
129
+
130
+ def parsed_time
131
+ @parsed_time ||= ::DateTime.parse(time)
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,107 @@
1
+ require 'forwardable'
2
+ require 'dotiw'
3
+
4
+ require_relative 'command/date_time'
5
+
6
+ module Cease
7
+ module Eviction
8
+ class Comment
9
+ extend Forwardable
10
+
11
+ include Comparable
12
+ include DOTIW::Methods
13
+
14
+ # @examples
15
+ # [cease] at 14:00
16
+ # [cease] at 1pm on 01/01/2044
17
+ # [cease] at 2:30pm on 12/01/2021 {timezone: 'PST'}
18
+ OPEN_COMMENT_REGEX = /
19
+ \[cease\]\s # prefix
20
+ (.+?) # non-greedy date and time
21
+ (:?\s*) # optional seperator
22
+ (\{.*?\})? # optional options
23
+ $ # extend non-greedy matchers to end of line
24
+ /x.freeze
25
+ CLOSE_COMMENT_REGEX = /\[\/cease\]/.freeze
26
+
27
+ def self.close_comment?(comment)
28
+ return false unless comment
29
+ !!(comment.text =~ CLOSE_COMMENT_REGEX)
30
+ end
31
+
32
+ # @params comment [Parser::Source::Comment]
33
+ # @params source [Pathname]
34
+ def initialize(comment:, source: nil)
35
+ @comment = comment
36
+ @date_time, @seperator, @options = scanned_comment
37
+ @source = source
38
+ end
39
+
40
+ def parse
41
+ scanned_comment
42
+ end
43
+
44
+ def date_time
45
+ Command::DateTime.new(self, @date_time, @options)
46
+ end
47
+
48
+ def close_comment?
49
+ self.class.close_comment?(comment)
50
+ end
51
+
52
+ def last_commit_date
53
+ return unless source
54
+
55
+ line = loc.line
56
+ search = "-L #{line},#{line}:#{source.to_s}"
57
+ Git.log.object(search)&.first&.date
58
+ end
59
+
60
+ def past_due_description
61
+ return unless overdue?
62
+
63
+ dotiw = distance_of_time_in_words(
64
+ date_time.tz.to_local(DateTime.now),
65
+ date_time.parsed_in_timezone,
66
+ highest_measures: 1
67
+ )
68
+
69
+ "Overdue by roughly #{dotiw}"
70
+ end
71
+
72
+ def overdue?
73
+ return false if close_comment?
74
+ return false unless date_time.valid?
75
+
76
+ date_time.tz.to_local(DateTime.now) >= date_time.parsed_in_timezone
77
+ end
78
+
79
+ def <=>(other)
80
+ other.loc.expression.begin_pos <=> loc.expression.begin_pos
81
+ end
82
+
83
+ def nested_in?(other)
84
+ other.loc.expression.end_pos > loc.expression.begin_pos
85
+ end
86
+
87
+ def valid?
88
+ return false unless comment
89
+ return true if close_comment?
90
+ parse.any?
91
+ end
92
+
93
+ attr_reader :comment, :options, :source
94
+
95
+ private
96
+
97
+ delegate loc: :comment
98
+
99
+ def scanned_comment
100
+ return [] unless comment
101
+
102
+ @scanned_comment ||=
103
+ comment.text.scan(OPEN_COMMENT_REGEX).flatten
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,100 @@
1
+ require 'parser/current'
2
+
3
+ require_relative 'chunk'
4
+ require_relative 'comment'
5
+ require_relative 'scope'
6
+ require_relative 'statement'
7
+
8
+ module Cease
9
+ module Eviction
10
+ class Context
11
+ class << self
12
+ # @params source [Pathname]
13
+ #
14
+ # @return [Array<Cease::Eviction>]
15
+ def from_source(source:)
16
+ source_to_chunks(source) do |chunk, statement, comments|
17
+ new(comments: comments, statement: statement, chunk: chunk)
18
+ end.compact
19
+ end
20
+
21
+ private
22
+
23
+ def parsed_source(source)
24
+ buffer = Parser::Source::Buffer.new(source.to_s, 1)
25
+ buffer.read
26
+
27
+ Parser::CurrentRuby.new.parse_with_comments(buffer)
28
+ end
29
+
30
+ def closest_close_comment(comments, index)
31
+ # Find the next closest close command.
32
+ # Nested evictions break things.
33
+ comments[index..-1].find do |comment|
34
+ Comment.close_comment?(comment)
35
+ end
36
+ end
37
+
38
+ def source_to_chunks(source)
39
+ ast, comments = parsed_source(source)
40
+
41
+ comments.each_with_index.map do |comment, index|
42
+ next if Comment.close_comment?(comment)
43
+
44
+ statement = Statement.from_comments(
45
+ comment,
46
+ closest_close_comment(comments, index + 1),
47
+ source
48
+ )
49
+
50
+ next unless statement.valid?
51
+
52
+ chunk = Chunk.new(ast: ast, statement: statement)
53
+ yield(chunk, statement, comments) if block_given?
54
+ end
55
+ end
56
+ end
57
+
58
+ def initialize(chunk:, comments:, statement:)
59
+ @chunk = chunk
60
+ @comments = comments
61
+ @statement = statement
62
+ end
63
+
64
+ def description
65
+ indent = ' ' * 2
66
+ lines_output = lines.inspect
67
+ alignment = ' ' * (lines_output.length - indent.length)
68
+ result = ''
69
+
70
+ header = "#{indent}#{Rainbow(lines_output).blue}: "\
71
+ "#{Rainbow(statement.open_comment.past_due_description).indianred}\n"
72
+
73
+ scope.format.each do |line|
74
+ result << "#{alignment}#{line}\n"
75
+ end
76
+
77
+ "#{header}#{Rainbow(result).wheat}\n"
78
+ end
79
+
80
+ def lines
81
+ return [] unless statement.valid?
82
+ statement.lines
83
+ end
84
+
85
+ def overdue?
86
+ return false unless statement.valid?
87
+ statement.open_comment.overdue?
88
+ end
89
+
90
+ attr_reader :comments, :statement, :chunk
91
+
92
+ private
93
+
94
+ def scope
95
+ @scope ||=
96
+ Scope.new(chunk: chunk, comments: comments, statement: statement)
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,83 @@
1
+ require 'forwardable'
2
+
3
+ require 'rainbow'
4
+
5
+ module Cease
6
+ module Eviction
7
+ class Scope
8
+ extend Forwardable
9
+
10
+ # @params chunk [Cease::Eviction::Chunk]
11
+ # @params comments [Array<Parser::Source::Comment>]
12
+ # @params statement [Cease::Statement>]
13
+ def initialize(chunk:, comments:, statement:)
14
+ @chunk = chunk
15
+ @comments = comments
16
+ @statement = statement
17
+ end
18
+
19
+ # @return [Array<String>]
20
+ def format
21
+ length = formatted_lines.length
22
+
23
+ case length
24
+ when (0..25) then formatted_lines
25
+ else
26
+ [
27
+ formatted_lines[0..10],
28
+ formatted_lines[11] << "\n",
29
+ Rainbow("...#{line_count - 12} line(s) truncated.").yellow
30
+ ].flatten
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :chunk, :comments, :statement
37
+
38
+ delegate [:open_comment, :close_comment] => :statement
39
+
40
+ # @return [Array<Parser::AST::Node, Parser::Source::Comment>]
41
+ def chunk_with_comments
42
+ extracted_chunk = chunk.extract
43
+ sorted_comments = comments.sort_by { |comment| comment.loc.line }
44
+
45
+ sorted_scope = extracted_chunk.each_with_object([]) do |ast, results|
46
+ until sorted_comments.first.loc.line > ast.loc.line do
47
+ comment = sorted_comments.shift
48
+
49
+ results << comment if comment.loc.line > open_comment.comment.loc.line
50
+ end
51
+
52
+ results << ast
53
+ end
54
+
55
+ # Concatenate any leftover comments outside AST nodes.
56
+ sorted_comments.each do |comment|
57
+ sorted_scope << comment if comment.loc.line < close_comment.comment.loc.line
58
+ end
59
+
60
+ sorted_scope
61
+ end
62
+
63
+ # @note Align indented lines relative to the expression's column.
64
+ def formatted_lines
65
+ @formatted_lines ||= chunk_with_comments.map do |content|
66
+ column = content.loc.column
67
+
68
+ # TODO: Find single line breaks between lines.
69
+ content.loc.expression.source
70
+ .split("\n")
71
+ .map do |result|
72
+ next result[column...] if result.start_with?(' ')
73
+ result
74
+ end
75
+ end.flatten
76
+ end
77
+
78
+ def line_count
79
+ statement.lines.inject(&:-).abs
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,39 @@
1
+ require_relative 'comment'
2
+
3
+ module Cease
4
+ module Eviction
5
+ class Statement
6
+ def self.from_comments(*comments)
7
+ open_comment, close_comment, source = comments
8
+
9
+ new(
10
+ open_comment: open_comment,
11
+ close_comment: close_comment,
12
+ source: source
13
+ )
14
+ end
15
+
16
+ # @param open_comment [Parser::Source::Comment]
17
+ # @param close_comment [Parser::Source::Comment]
18
+ # @params source [Pathname]
19
+ def initialize(open_comment:, close_comment:, source:)
20
+ @open_comment = Comment.new(comment: open_comment, source: source)
21
+ @close_comment = Comment.new(comment: close_comment, source: source)
22
+ end
23
+
24
+ def lines
25
+ return [] unless valid?
26
+
27
+ [open_comment, close_comment].map do |eviction_comment|
28
+ eviction_comment.loc&.line
29
+ end
30
+ end
31
+
32
+ def valid?
33
+ [open_comment, close_comment].all?(&:valid?)
34
+ end
35
+
36
+ attr_reader :open_comment, :close_comment
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
1
+ require_relative 'summary'
2
+ require_relative 'eviction/context'
3
+
4
+ module Cease
5
+ class Examiner
6
+ # @param source [Pathname]
7
+ def initialize(source:)
8
+ @source = source
9
+ end
10
+
11
+ def evictions
12
+ @evictions ||= Eviction::Context.from_source(source: source)
13
+ end
14
+
15
+ def summarize
16
+ return unless summarizable?
17
+ Summary.new(examiner: self).summarize
18
+ end
19
+
20
+ def summarizable?
21
+ overdue_evictions.any?
22
+ end
23
+
24
+ def overdue_evictions
25
+ evictions.select(&:overdue?)
26
+ end
27
+
28
+ attr_reader :source
29
+ end
30
+ end
data/lib/cease/git.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'git'
2
+
3
+ module Cease
4
+ class Git
5
+ def self.log
6
+ new.log
7
+ end
8
+
9
+ def initialize(pwd: Pathname.pwd.to_s)
10
+ @pwd = pwd
11
+ end
12
+
13
+ def log
14
+ @log ||= ::Git.open(pwd).log
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :pwd
20
+ end
21
+ end
@@ -0,0 +1,68 @@
1
+ require_relative 'examiner'
2
+
3
+ module Cease
4
+ class Report
5
+ SUCCESS_EXIT_CODE = 0
6
+ ERROR_EXIT_CODE = 1
7
+
8
+ def initialize(sources:)
9
+ @sources = sources
10
+ @examiners = []
11
+ @total_eviction_count = 0
12
+ end
13
+
14
+ def execute
15
+ print_header
16
+
17
+ sources.each do |source|
18
+ add_examiner(Examiner.new(source: source))
19
+ end
20
+
21
+ print_results
22
+ print_footer if reportable?
23
+
24
+ result_code
25
+ end
26
+
27
+ attr_accessor :total_eviction_count
28
+
29
+ private
30
+
31
+ attr_reader :examiners, :sources
32
+
33
+ def add_examiner(examiner)
34
+ self.total_eviction_count += examiner.overdue_evictions.length
35
+ examiners << examiner
36
+ end
37
+
38
+ def print_results
39
+ summarizable_examiners.each(&:summarize)
40
+ end
41
+
42
+ def print_header
43
+ puts "\nScanning #{sources.length} source(s)...\n\n"
44
+ end
45
+
46
+ def print_footer
47
+ puts Rainbow(
48
+ Rainbow"\nTotal of #{total_eviction_count} evictions(s) found.\n"
49
+ ).green
50
+ end
51
+
52
+ def result_code
53
+ if reportable?
54
+ ERROR_EXIT_CODE
55
+ else
56
+ SUCCESS_EXIT_CODE
57
+ end
58
+ end
59
+
60
+ def reportable?
61
+ summarizable_examiners.any?
62
+ end
63
+
64
+ def summarizable_examiners
65
+ examiners.select(&:summarizable?)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,33 @@
1
+ require 'forwardable'
2
+
3
+ require 'rainbow'
4
+
5
+ module Cease
6
+ class Summary
7
+ extend Forwardable
8
+
9
+ delegate [
10
+ :source,
11
+ :overdue_evictions
12
+ ] => :examiner
13
+
14
+ def initialize(examiner:)
15
+ @examiner = examiner
16
+ end
17
+
18
+ def summarize
19
+ return if overdue_evictions.none?
20
+
21
+ puts Rainbow("(#{source_name})").underline.bright
22
+ puts "\n#{overdue_evictions.map(&:description).join}"
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :examiner
28
+
29
+ def source_name
30
+ source.to_s
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module Cease
2
+ VERSION = "0.1.1"
3
+ end
data/lib/cease.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'tzinfo'
2
+
3
+ require "cease/cli"
4
+ require "cease/examiner"
5
+ require "cease/git"
6
+ require "cease/report"
7
+ require "cease/summary"
8
+ require "cease/version"
9
+
10
+ module Cease
11
+ end
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cease
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Sung Noh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-01-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 3.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 3.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: git
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.10.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.10.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rainbow
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 3.0.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 3.0.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: dotiw
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 5.3.2
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 5.3.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: tzinfo
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 2.0.4
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 2.0.4
83
+ description: Cease is a tool that detects blocks of code to be removed at a specified
84
+ time.
85
+ email:
86
+ - sung@dustybit.software
87
+ executables:
88
+ - cease
89
+ - console
90
+ - setup
91
+ extensions: []
92
+ extra_rdoc_files: []
93
+ files:
94
+ - ".github/workflows/ci.yml"
95
+ - ".gitignore"
96
+ - ".rspec"
97
+ - CODE_OF_CONDUCT.md
98
+ - Gemfile
99
+ - LICENSE.txt
100
+ - README.md
101
+ - Rakefile
102
+ - bin/cease
103
+ - bin/console
104
+ - bin/setup
105
+ - cease.gemspec
106
+ - lib/cease.rb
107
+ - lib/cease/cli.rb
108
+ - lib/cease/eviction/chunk.rb
109
+ - lib/cease/eviction/command/date_time.rb
110
+ - lib/cease/eviction/comment.rb
111
+ - lib/cease/eviction/context.rb
112
+ - lib/cease/eviction/scope.rb
113
+ - lib/cease/eviction/statement.rb
114
+ - lib/cease/examiner.rb
115
+ - lib/cease/git.rb
116
+ - lib/cease/report.rb
117
+ - lib/cease/summary.rb
118
+ - lib/cease/version.rb
119
+ homepage: https://github.com/DustyBitSoftware/cease
120
+ licenses:
121
+ - BSD-3-Clause
122
+ metadata:
123
+ homepage_uri: https://github.com/DustyBitSoftware/cease
124
+ source_code_uri: https://github.com/DustyBitSoftware/cease
125
+ changelog_uri: https://github.com/DustyBitSoftware/cease/CHANGELOG.md
126
+ post_install_message:
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: 2.3.0
135
+ required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubygems_version: 3.2.32
142
+ signing_key:
143
+ specification_version: 4
144
+ summary: Evict unused code at some time in the future
145
+ test_files: []