cease 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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: []