debtective 0.2.3.3 → 0.2.3.6

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
2
  SHA256:
3
- metadata.gz: f5c39390d5b43a335045481e47b106c30facd0d66ea88359209ecc57887f6a4e
4
- data.tar.gz: 182beede8ace506b42e175bdd9fec0748464bfe4aa75ff1e95c7d100df4c5444
3
+ metadata.gz: ebde46135dabc88a0da1b27f4134cd932bebf3c329dc1e894413f4dd914caa15
4
+ data.tar.gz: d0a7a1181c6766d0aaac682ab1fcfe1433e2a7ced12cf14b86d0c72d7c7a9b0b
5
5
  SHA512:
6
- metadata.gz: c846a7c047b667926d576a51331820b26e98d8e25ff16bdb192a2c8aa080aceac69f9cb7f747c90d8e3c25ed6c7c59bd3fbd6642a2da2572ffeeb304c54aa10d
7
- data.tar.gz: 4870ac4fcc0b9fa93eeaf2f72e410b06bd7b9bcea56e44557f32ee2788c572186cdea8177a6168c24cc6917360e5d02e90793b951d5d1bcdb58806cad00ca28b
6
+ metadata.gz: ff8049df8035520a01ee0f3f5f9f6287c9e1575596123dec02e1720c5150c1053bf5e821a29c1886c77bf9a29b3cf946c5534d749c7b502519f1e5cfab06836d
7
+ data.tar.gz: 4cbacb9e39e81170c37a61c1e0d0dd585014451f53e560f5a12f0bfca60e5db91faa3cf441eaddc142cd2f9d595fe5e69b7b117216d8068aab977843bf800412
data/bin/debtective CHANGED
@@ -1,23 +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/output"
18
- Debtective::Todos::Output.new(user_name, quiet: quiet).call
19
- when "--offenses", "--gems"
20
- puts "Upcoming feature"
21
- else
22
- puts "Please pass one of this options: [--todos, --offenses, --gems]"
23
- 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
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Debtective
4
+ module Comments
5
+ # Find the index of the line ending a statement
6
+ #
7
+ # @example
8
+ # FindEndOfStatement.new(
9
+ # [
10
+ # "class User",
11
+ # " def example",
12
+ # " x + y",
13
+ # " end"
14
+ # "end"
15
+ # ],
16
+ # 1
17
+ # ).call
18
+ # => 3
19
+ #
20
+ class FindEndOfStatement
21
+ # @param lines [Array<String>] lines of code
22
+ # @param first_line_index [Integer] index of the statement first line
23
+ def initialize(lines:, first_line_index:)
24
+ @lines = lines
25
+ @first_line_index = first_line_index
26
+ end
27
+
28
+ # @return [Integer] index of the line ending the statement
29
+ # @note suppress_stderr prevents outputs from RubyVM::InstructionSequence.compile
30
+ def call
31
+ last_line_index
32
+ end
33
+
34
+ private
35
+
36
+ # Recursively find index of the line ending the statement
37
+ # (it is possible that no line ends the statement
38
+ # especially if the first line is not the start of a statement)
39
+ def last_line_index(index = @first_line_index)
40
+ return @first_line_index if index >= @lines.size
41
+ return index if !chained?(index) && statement?(index)
42
+
43
+ last_line_index(index + 1)
44
+ end
45
+
46
+ # Check if the code from first index to given index is a statement,
47
+ # e.i. can be parsed by a ruby parser (using whitequark/parser)
48
+ def statement?(index)
49
+ code = @lines[@first_line_index..index].join("\n")
50
+ RubyVM::InstructionSequence.compile(code)
51
+ rescue SyntaxError
52
+ false
53
+ end
54
+
55
+ # Check if current line is chained, e.i. next line start with a "."
56
+ def chained?(index)
57
+ @lines[index + 1]&.match?(/^(\s*)\./)
58
+ end
59
+ end
60
+ end
61
+ end