Skip to content

Instantly share code, notes, and snippets.

@ohthatjames
Last active January 30, 2019 16:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ohthatjames/10d21dc75424b9bd67fc71bc19f7863a to your computer and use it in GitHub Desktop.
Save ohthatjames/10d21dc75424b9bd67fc71bc19f7863a to your computer and use it in GitHub Desktop.
Stats of rails project over time
ruby -e 'require "date"; f=File.open("results.txt", "w"); (2013..2019).to_a.reverse.map{|y| (1..12).to_a.reverse.each{|m| d = Date.new(y, m, 1); next if d < Date.new(2013, 4, 3) || d > Date.new(2019, 2, 1); d = d.strftime("%Y-%m-%d"); commit = `git log --before=#{d} -n 1 --format=format:"%H"`.chomp; f.puts "#{d}: #{commit}"; f.puts `git checkout #{commit} && ruby stats.rb` }}'
require "active_support/core_ext/enumerable"
require "active_support/core_ext/string/inflections"
class CodeStatisticsCalculator #:nodoc:
attr_reader :lines, :code_lines, :classes, :methods
PATTERNS = {
rb: {
line_comment: /^\s*#/,
begin_block_comment: /^=begin/,
end_block_comment: /^=end/,
class: /^\s*class\s+[_A-Z]/,
method: /^\s*def\s+[_a-z]/,
},
js: {
line_comment: %r{^\s*//},
begin_block_comment: %r{^\s*/\*},
end_block_comment: %r{\*/},
method: /function(\s+[_a-zA-Z][\da-zA-Z]*)?\s*\(/,
},
coffee: {
line_comment: /^\s*#/,
begin_block_comment: /^\s*###/,
end_block_comment: /^\s*###/,
class: /^\s*class\s+[_A-Z]/,
method: /[-=]>/,
}
}
PATTERNS[:minitest] = PATTERNS[:rb].merge method: /^\s*(def|test)\s+['"_a-z]/
PATTERNS[:rake] = PATTERNS[:rb]
def initialize(lines = 0, code_lines = 0, classes = 0, methods = 0)
@lines = lines
@code_lines = code_lines
@classes = classes
@methods = methods
end
def add(code_statistics_calculator)
@lines += code_statistics_calculator.lines
@code_lines += code_statistics_calculator.code_lines
@classes += code_statistics_calculator.classes
@methods += code_statistics_calculator.methods
end
def add_by_file_path(file_path)
File.open(file_path) do |f|
add_by_io(f, file_type(file_path))
end
end
def add_by_io(io, file_type)
patterns = PATTERNS[file_type] || {}
comment_started = false
while line = io.gets
@lines += 1
if comment_started
if patterns[:end_block_comment] && line =~ patterns[:end_block_comment]
comment_started = false
end
next
else
if patterns[:begin_block_comment] && line =~ patterns[:begin_block_comment]
comment_started = true
next
end
end
@classes += 1 if patterns[:class] && line =~ patterns[:class]
@methods += 1 if patterns[:method] && line =~ patterns[:method]
if line !~ /^\s*$/ && (patterns[:line_comment].nil? || line !~ patterns[:line_comment])
@code_lines += 1
end
end
end
private
def file_type(file_path)
if file_path.end_with? "_test.rb"
:minitest
else
File.extname(file_path).sub(/\A\./, "").downcase.to_sym
end
end
end
class CodeStatistics #:nodoc:
TEST_TYPES = ["Controller tests",
"Helper tests",
"Model tests",
"Mailer tests",
"Job tests",
"Integration tests",
"System tests"]
HEADERS = { lines: " Lines", code_lines: " LOC", classes: "Classes", methods: "Methods" }
def initialize(*pairs)
@pairs = pairs
@statistics = calculate_statistics
@total = calculate_total if pairs.length > 1
end
def to_s
print_header
@pairs.each { |pair| print_line(pair.first, @statistics[pair.first]) }
print_splitter
if @total
print_line("Total", @total)
print_splitter
end
print_code_test_stats
end
private
def calculate_statistics
Hash[@pairs.map { |pair| [pair.first, calculate_directory_statistics(pair.last)] }]
end
def calculate_directory_statistics(directory, pattern = /^(?!\.).*?\.(rb|js|coffee|rake)$/)
stats = CodeStatisticsCalculator.new
Dir.foreach(directory) do |file_name|
path = "#{directory}/#{file_name}"
if File.directory?(path) && (/^\./ !~ file_name)
stats.add(calculate_directory_statistics(path, pattern))
elsif file_name =~ pattern
stats.add_by_file_path(path)
end
end
stats
end
def calculate_total
@statistics.each_with_object(CodeStatisticsCalculator.new) do |pair, total|
total.add(pair.last)
end
end
def calculate_code
code_loc = 0
@statistics.each { |k, v| code_loc += v.code_lines unless TEST_TYPES.include? k }
code_loc
end
def calculate_tests
test_loc = 0
@statistics.each { |k, v| test_loc += v.code_lines if TEST_TYPES.include? k }
test_loc
end
def width_for(label)
[@statistics.values.sum { |s| s.send(label) }.to_s.size, HEADERS[label].length].max
end
def print_header
print_splitter
print "| Name "
HEADERS.each do |k, v|
print " | #{v.rjust(width_for(k))}"
end
puts " | M/C | LOC/M |"
print_splitter
end
def print_splitter
print "+----------------------"
HEADERS.each_key do |k|
print "+#{'-' * (width_for(k) + 2)}"
end
puts "+-----+-------+"
end
def print_line(name, statistics)
m_over_c = (statistics.methods / statistics.classes) rescue m_over_c = 0
loc_over_m = (statistics.code_lines / statistics.methods) - 2 rescue loc_over_m = 0
print "| #{name.ljust(20)} "
HEADERS.each_key do |k|
print "| #{statistics.send(k).to_s.rjust(width_for(k))} "
end
puts "| #{m_over_c.to_s.rjust(3)} | #{loc_over_m.to_s.rjust(5)} |"
end
def print_code_test_stats
code = calculate_code
tests = calculate_tests
puts " Code LOC: #{code} Test LOC: #{tests} Code to Test Ratio: 1:#{sprintf("%.1f", tests.to_f / code)}"
puts ""
end
end
STATS_DIRECTORIES = [
%w(Controllers app/controllers),
%w(Helpers app/helpers),
%w(Jobs app/jobs),
%w(Models app/models),
%w(Mailers app/mailers),
%w(Channels app/channels),
%w(JavaScripts app/assets/javascripts),
%w(Libraries lib/),
%w(APIs app/apis),
%w(Controller\ tests test/controllers),
%w(Helper\ tests test/helpers),
%w(Model\ tests test/models),
%w(Mailer\ tests test/mailers),
%w(Job\ tests test/jobs),
%w(Integration\ tests test/integration),
%w(System\ tests test/system),
].collect do |name, dir|
[ name, "./#{dir}" ]
end.select { |name, dir| File.directory?(dir) }
spec_types = begin
dirs = Dir['./spec/**/*_spec.rb'].
map { |f| f.sub(/^\.\/(spec\/\w+)\/.*/, '\\1') }.
uniq.
select { |f| File.directory?(f) }
Hash[dirs.map { |d| [d.split('/').last, d] }]
end
spec_types.each do |type, dir|
name = type.singularize.capitalize
::STATS_DIRECTORIES << ["#{name} specs", dir]
::CodeStatistics::TEST_TYPES << "#{name} specs"
end
CodeStatistics.new(*STATS_DIRECTORIES).to_s
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment