debtective 0.2.3.4 → 0.2.3.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09e2ba02faff3be09a87e2babe120bb59e28f0ea509ad8cc8ef1f9a12cf55ae6'
4
- data.tar.gz: e1c5d6197706783907a1756671928eac854d1d771eb541d58b18dba8053da003
3
+ metadata.gz: 0ca6f9979cf0b568bf7abd46ec7bca857f7fd2e62a8d15e85cd80920450395a7
4
+ data.tar.gz: c7df027f440b683be96278a5c57ae8b1829ac261b861a11b68d78b2c502be2cf
5
5
  SHA512:
6
- metadata.gz: f1641ef5d5031d12d7c85665a252ab168cf456297621b81c400f22c8a8017868eb3c7873eade9ae2c8b0cb21c93a85d702ff16ffd4a45b822edd0d450ca42835
7
- data.tar.gz: f371a5a826c7daff0c6cc2a31e7e5686731f29d74b568e96174de0000bdec70c62318481ff2a21ef98ff84d279053fbbb747511f75b0e0c79c8adb160f540bcb
6
+ metadata.gz: b02f93cac47d43efbd15521a6be821d9de597b8d56d14980a42456211b57f9c6c06161d4aae27f66b2bcc47bee034112dd721e232f87bb698eacef504ce1ea0e
7
+ data.tar.gz: f7e4f1cd2af557f31e4e36ec89adb77af8eecffe6dfdfeb3ea6df5ff3d40c9d0aca0a66a7b9b6fe786b6e0883f0d8f81a01c4b5b0301904ae1f0f8bab9a3b54e
data/bin/debtective CHANGED
@@ -1,26 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "debtective"
4
+ require "debtective/command"
5
5
 
6
- user_name =
7
- if ARGV.include?("--me")
8
- `git config user.name`.strip
9
- elsif ARGV.include?("--user")
10
- ARGV[ARGV.index("--user") + 1]
11
- end
12
-
13
- quiet = ARGV.include?("--quiet")
14
-
15
- case ARGV[0]
16
- when "--todos"
17
- require "debtective/todos/export"
18
- Debtective::Todos::Export.new(user_name: user_name, quiet: quiet).call
19
- when "--offenses"
20
- require "debtective/offenses/export"
21
- Debtective::Offenses::Export.new(user_name: user_name, quiet: quiet).call
22
- when "--gems"
23
- puts "Upcoming feature"
24
- else
25
- puts "Please pass one of this options: [--todos, --offenses, --gems]"
26
- end
6
+ Debtective::Command.new(ARGV).call
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "debtective"
4
+ require_relative "comments/command"
5
+
6
+ module Debtective
7
+ # Handle commands from CLI
8
+ class Command
9
+ # @param args [Array<String>] ARGVs from command line (order matters)
10
+ def initialize(args)
11
+ @args = args
12
+ end
13
+
14
+ # Forward to the proper command
15
+ def call
16
+ case @args.first&.delete("--")
17
+ when "comments"
18
+ Debtective::Comments::Command.new(@args, quiet: quiet?).call
19
+ when "gems"
20
+ puts "Upcoming feature!"
21
+ else
22
+ puts "Please pass one of this options: [--comments, --gems]"
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def quiet?
29
+ @args.include?("--quiet")
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "comment/fixme"
4
+ require_relative "comment/magic"
5
+ require_relative "comment/note"
6
+ require_relative "comment/offense"
7
+ require_relative "comment/shebang"
8
+ require_relative "comment/todo"
9
+ require_relative "comment/yard"
10
+
11
+ module Debtective
12
+ module Comments
13
+ # Build proper comment type given the code line
14
+ class BuildComment
15
+ # Order matters, for example the Comment::Note regex matches
16
+ # (almost) all previous regexes so it needs to be tested last
17
+ TYPES = {
18
+ /#\sTODO:/ => Comment::Todo,
19
+ /#\sFIXME:/ => Comment::Fixme,
20
+ /\s# rubocop:disable (.*)/ => Comment::Offense,
21
+ /#\s@/ => Comment::Yard,
22
+ /^# (frozen_string_literal|coding|encoding|warn_indent|sharable_constant_value):/ => Comment::Magic,
23
+ /^#!/ => Comment::Shebang,
24
+ /(^|\s)#\s/ => Comment::Note
25
+ }.freeze
26
+
27
+ # @param line [String] code line
28
+ # @param pathname [Pathname] file path
29
+ # @param index [Integer] position of the line in the file
30
+ def initialize(line:, pathname:, index:)
31
+ @line = line
32
+ @pathname = pathname
33
+ @index = index
34
+ end
35
+
36
+ # @return [Debtective::Comments::Base]
37
+ def call
38
+ klass&.new(pathname: @pathname, index: @index)
39
+ end
40
+
41
+ private
42
+
43
+ def klass
44
+ TYPES.each { |regex, klass| return klass if @line.match?(regex) }
45
+ nil
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "export"
4
+
5
+ module Debtective
6
+ module Comments
7
+ # Handle comments command and given ARGVs
8
+ class Command
9
+ TYPE_OPTIONS = %w[fixme magic note offense shebang todo yard].freeze
10
+
11
+ # @param args [Array<String>] ARGVs from command line (order matters)
12
+ # @param quiet [Boolean]
13
+ def initialize(args, quiet: false)
14
+ @args = args
15
+ @quiet = quiet
16
+ end
17
+
18
+ # @return [Debtective::Comments::Export]
19
+ def call
20
+ Export.new(
21
+ user_name: user_name,
22
+ quiet: @quiet,
23
+ included_types: included_types,
24
+ excluded_types: excluded_types,
25
+ included_paths: paths("include"),
26
+ excluded_paths: paths("exclude")
27
+ ).call
28
+ end
29
+
30
+ private
31
+
32
+ def user_name
33
+ if @args.include?("--me")
34
+ `git config user.name`.strip
35
+ elsif @args.include?("--user")
36
+ @args[@args.index("--user") + 1]
37
+ end
38
+ end
39
+
40
+ def included_types
41
+ TYPE_OPTIONS.select { @args.include?("--#{_1}") }
42
+ end
43
+
44
+ def excluded_types
45
+ TYPE_OPTIONS.select { @args.include?("--no-#{_1}") }
46
+ end
47
+
48
+ def paths(rule)
49
+ return [] unless @args.include?("--#{rule}")
50
+
51
+ paths = []
52
+ @args[(@args.index("--#{rule}") + 1)..].each do |arg|
53
+ return paths if arg.match?(/^--.*/)
54
+
55
+ paths << arg
56
+ end
57
+ paths
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../find_commit"
4
+
5
+ module Debtective
6
+ module Comments
7
+ module Comment
8
+ # Hold comment information
9
+ class Base
10
+ # Does not match YARD or shebang comments
11
+ REGULAR_COMMENT = /^\s*#\s(?!@)/
12
+
13
+ attr_accessor :pathname
14
+
15
+ # @param pathname [Pathname] path of the file
16
+ # @param index [Integer] position of the comment in the file
17
+ def initialize(pathname:, index:)
18
+ @pathname = pathname
19
+ @index = index
20
+ end
21
+
22
+ # @return [String]
23
+ def type
24
+ self.class.name.split("::").last.downcase
25
+ end
26
+
27
+ # @return [Integer]
28
+ def comment_start
29
+ @index
30
+ end
31
+
32
+ # @return [Integer]
33
+ def comment_end
34
+ @comment_end ||= last_following_comment_index || comment_start
35
+ end
36
+
37
+ # @return [Integer]
38
+ def statement_start
39
+ @statement_start ||= inline? ? @index : comment_end.next
40
+ end
41
+
42
+ # @return [Integer]
43
+ def statement_end
44
+ statement_start
45
+ end
46
+
47
+ # Location in the codebase (for clickable link)
48
+ # @return [String]
49
+ def location
50
+ "#{@pathname.to_s.gsub(%r{^./}, "")}:#{comment_start + 1}"
51
+ end
52
+
53
+ # Return commit that introduced the todo
54
+ # @return [Debtective::Comments::FindCommit::Commit]
55
+ def commit
56
+ @commit ||= FindCommit.new(pathname: @pathname, line: lines[@index]).call
57
+ end
58
+
59
+ # @return [Integer]
60
+ def days
61
+ return if commit.time.nil?
62
+
63
+ ((Time.now - commit.time) / (24 * 60 * 60)).round
64
+ end
65
+
66
+ # @return [Hash]
67
+ def to_h
68
+ {
69
+ pathname: @pathname,
70
+ location: location,
71
+ type: type,
72
+ comment_boundaries: [comment_start, comment_end],
73
+ statement_boundaries: [statement_start, statement_end],
74
+ commit: commit.sha,
75
+ author: commit.author.to_h,
76
+ time: commit.time
77
+ }
78
+ end
79
+
80
+ private
81
+
82
+ def lines
83
+ @lines ||= @pathname.readlines
84
+ end
85
+
86
+ def inline?
87
+ @inline ||= !lines[@index].match?(/^\s*#/)
88
+ end
89
+
90
+ def last_following_comment_index
91
+ if inline?
92
+ @index
93
+ else
94
+ lines.index.with_index do |line, i|
95
+ i > @index &&
96
+ !line.strip.empty? &&
97
+ !line.match?(REGULAR_COMMENT)
98
+ end&.-(1)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../find_end_of_statement"
5
+
6
+ module Debtective
7
+ module Comments
8
+ module Comment
9
+ # Hold FIXME comment information
10
+ class Fixme < Base
11
+ # (see Base#statement_end)
12
+ def statement_end
13
+ return super if inline?
14
+
15
+ FindEndOfStatement.new(lines: lines, first_line_index: statement_start).call
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Debtective
6
+ module Comments
7
+ module Comment
8
+ # Hold magic comment information
9
+ class Magic < Base
10
+ # (see Base#comment_end)
11
+ def comment_end
12
+ @index
13
+ end
14
+
15
+ # (see Base#statement_start)
16
+ def statement_start
17
+ nil
18
+ end
19
+
20
+ # (see Base#statement_end)
21
+ def statement_end
22
+ nil
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Debtective
6
+ module Comments
7
+ module Comment
8
+ # Hold note comment information (any "other" comment)
9
+ class Note < Base; end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Debtective
6
+ module Comments
7
+ module Comment
8
+ # Hold offense information
9
+ class Offense < Base; end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Debtective
6
+ module Comments
7
+ module Comment
8
+ # Hold shebang comment (#!) information
9
+ class Shebang < Base
10
+ # (see Base#comment_end)
11
+ def comment_end
12
+ @index
13
+ end
14
+
15
+ # (see Base#statement_start)
16
+ def statement_start
17
+ nil
18
+ end
19
+
20
+ # (see Base#statement_end)
21
+ def statement_end
22
+ nil
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../find_end_of_statement"
5
+
6
+ module Debtective
7
+ module Comments
8
+ module Comment
9
+ # Hold TODO comment information
10
+ class Todo < Base
11
+ # (see Base#statement_end)
12
+ def statement_end
13
+ return super if inline?
14
+
15
+ FindEndOfStatement.new(lines: lines, first_line_index: statement_start).call
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../find_end_of_statement"
5
+
6
+ module Debtective
7
+ module Comments
8
+ module Comment
9
+ # Hold YARD comment information
10
+ class Yard < Base
11
+ # (see Base#comment_end)
12
+ def comment_end
13
+ @index
14
+ end
15
+
16
+ # (see Base#statement_start)
17
+ def statement_start
18
+ last_following_comment_index.next
19
+ end
20
+
21
+ # (see Base#statement_end)
22
+ def statement_end
23
+ FindEndOfStatement.new(lines: lines, first_line_index: statement_start).call
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "json"
5
+ require "parser/current"
6
+ require_relative "../stderr_helper"
7
+ require_relative "build_comment"
8
+ require_relative "print"
9
+
10
+ module Debtective
11
+ module Comments
12
+ # Export comments in a JSON file and to stdout
13
+ class Export
14
+ include StderrHelper
15
+
16
+ OPTIONS = {
17
+ user_name: nil,
18
+ quiet: false,
19
+ included_types: [],
20
+ excluded_types: [],
21
+ included_paths: [],
22
+ excluded_paths: []
23
+ }.freeze
24
+
25
+ FILE_PATH = "comments.json"
26
+
27
+ # @param user_name [String] git user email to filter
28
+ # @param quiet [boolean]
29
+ # @param included_types [Array<String>] types of comment to export
30
+ # @param excluded_types [Array<String>] types of comment to skip
31
+ # @param included_paths [Array<String>] paths to explore
32
+ # @param excluded_paths [Array<String>] paths to skip
33
+ def initialize(**options)
34
+ OPTIONS.each do |key, default|
35
+ instance_variable_set(:"@#{key}", options[key] || default)
36
+ end
37
+ end
38
+
39
+ # @return [void]
40
+ def call
41
+ # suppress_stderr prevents stderr outputs from compilations
42
+ suppress_stderr do
43
+ @comments = @quiet ? find_comments : log_table
44
+ end
45
+ filter_comments!
46
+ log_counts unless @quiet
47
+ write_json_file
48
+ puts(FILE_PATH) unless @quiet
49
+ end
50
+
51
+ private
52
+
53
+ def find_comments
54
+ pathnames.flat_map do |pathname|
55
+ last_comment_found = nil
56
+ pathname.readlines.filter_map.with_index do |line, index|
57
+ next if last_comment_found && index < last_comment_found.comment_end.next
58
+ next unless comment?(line)
59
+
60
+ comment = BuildComment.new(line: line, pathname: pathname, index: index).call
61
+ next if comment.nil?
62
+
63
+ last_comment_found = comment
64
+ export_comment(comment)
65
+ end
66
+ end
67
+ end
68
+
69
+ def comment?(line)
70
+ return true if line =~ /^\s*#(\s|!)/
71
+ return false unless line =~ /\s(#\s.*)/
72
+
73
+ # Ensure it is really a comment to avoid something like
74
+ # puts("hello # world")
75
+ # to be considered as an inline comment
76
+ !Parser::CurrentRuby.parse(line).to_s.include?(Regexp.last_match[1])
77
+ rescue Parser::SyntaxError
78
+ false
79
+ end
80
+
81
+ # Export the comment if asked so by the user
82
+ # @return [Debtective::Comments::Comment::Base, nil]
83
+ # @note This method is watched by Print class to log in stdout
84
+ def export_comment(comment)
85
+ if @included_types.include?(comment.type) ||
86
+ (@included_types.empty? && !@excluded_types.include?(comment.type))
87
+
88
+ comment
89
+ end
90
+ end
91
+
92
+ def log_table
93
+ Print.new(user_name: @user_name).call { find_comments }
94
+ end
95
+
96
+ # List of paths to search in
97
+ def pathnames
98
+ Dir["./**/*"]
99
+ .map { Pathname(_1) }
100
+ .select { _1.file? && _1.extname == ".rb" && explore?(_1) }
101
+ end
102
+
103
+ def explore?(path)
104
+ return false if @excluded_paths.any? { path.to_s.gsub(%r{^./}, "").match?(/^#{_1}/) }
105
+ return true if @included_paths.empty?
106
+
107
+ @included_paths.any? { path.to_s.gsub(%r{^./}, "").match?(/^#{_1}/) }
108
+ end
109
+
110
+ # Select comments committed by the user
111
+ def filter_comments!
112
+ return if @user_name.nil?
113
+
114
+ @comments.select! { _1.commit.author.name == @user_name }
115
+ end
116
+
117
+ def log_counts
118
+ puts "total: #{@comments.count}"
119
+ end
120
+
121
+ def write_json_file
122
+ File.open(self.class::FILE_PATH, "w") do |file|
123
+ file.puts(
124
+ JSON.pretty_generate(
125
+ @comments.map(&:to_h)
126
+ )
127
+ )
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "git"
4
+ require "open3"
5
+
6
+ module Debtective
7
+ module Comments
8
+ # Find the commit that introduced the given line of code
9
+ class FindCommit
10
+ Author = Struct.new(:email, :name)
11
+ Commit = Struct.new(:sha, :author, :time)
12
+
13
+ # @param pathname [Pathname] file path
14
+ # @param line [String] line of code
15
+ def initialize(pathname:, line:)
16
+ @pathname = pathname
17
+ @line = line
18
+ end
19
+
20
+ # @return [Debtective::Comments::FindCommit::Commit]
21
+ def call
22
+ Commit.new(sha, author, time)
23
+ rescue Git::GitExecuteError
24
+ author = Author.new(nil, nil)
25
+ Commit.new(nil, author, nil)
26
+ end
27
+
28
+ # @return [Debtective::Comments::FindCommit::Author]
29
+ def author
30
+ Author.new(commit.author.email, commit.author.name)
31
+ end
32
+
33
+ # @return [Time]
34
+ def time
35
+ commit.date
36
+ end
37
+
38
+ # @return [String]
39
+ def sha
40
+ @sha ||=
41
+ begin
42
+ cmd = "git log -S \"#{safe_code}\" #{@pathname}"
43
+ stdout, _stderr, _status = ::Open3.capture3(cmd)
44
+ stdout[/commit (\w{40})\n/, 1]
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ # @return [Git::Base]
51
+ def git
52
+ Git.open(".")
53
+ end
54
+
55
+ # @return [Git::Object::Commit]
56
+ def commit
57
+ git.gcommit(sha)
58
+ end
59
+
60
+ # Characters " and ` can break the git command
61
+ def safe_code
62
+ @line.gsub(/"/, "\\\"").gsub("`", "\\\\`")
63
+ end
64
+ end
65
+ end
66
+ end