summaryrefslogtreecommitdiff
path: root/rake_helpers/code_statistics.rb
blob: 595848f770991ac3684d4e1ac63e64ff754d7452 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# From rails (http://rubyonrails.com)
#
# Improved by murphy
class CodeStatistics

  TEST_TYPES = /\btest/i

  # Create a new Code Statistic.
  #
  # Rakefile Example:
  #
  #  desc 'Report code statistics (LOC) from the application'
  #  task :stats => :copy_files do
  #    require 'rake_helpers/code_statistics'
  #    CodeStatistics.new(
  #      ["Main", "lib"],
  #      ["Tests", "test"],
  #      ["Demos", "demo"]
  #    ).to_s
  #   end
  def initialize(*pairs)
    @pairs = pairs
    @statistics = calculate_statistics
    @total = if pairs.empty? then nil else calculate_total end
  end

  # Print a textual table viewing the stats
  #
  # Intended for console output.
  def print
    print_header
    @pairs.each { |name, path| print_line name, @statistics[name] }
    print_splitter

    if @total
      print_line 'Total', @total
      print_splitter
    end

    print_code_test_stats
  end

private

  DEFAULT_FILE_PATTERN = /\.rb$/

  def calculate_statistics
    @pairs.inject({}) do |stats, (name, path, pattern, is_ruby_code)|
      pattern ||= DEFAULT_FILE_PATTERN
      path = File.join path, '*.rb'
      stats[name] = calculate_directory_statistics path, pattern, is_ruby_code
      stats
    end
  end

  def calculate_directory_statistics directory, pattern = DEFAULT_FILE_PATTERN, is_ruby_code = true
    is_ruby_code = true if is_ruby_code.nil?
    stats = Hash.new 0

    Dir[directory].each do |file_name|
      p "Scanning #{file_name}..." if $VERBOSE
      next unless file_name =~ pattern

      lines = codelines = classes = modules = methods = 0
      empty_lines = comment_lines = 0
      in_comment_block = false

      File.readlines(file_name).each do |line|
        lines += 1
        if line[/^\s*$/]
          empty_lines += 1
        elsif is_ruby_code
          case line
          when /^=end\b/
            comment_lines += 1
            in_comment_block = false
          when in_comment_block
            comment_lines += 1
          when /^\s*class\b/: classes += 1
          when /^\s*module\b/: modules += 1
          when /^\s*def\b/: methods += 1
          when /^\s*#/: comment_lines += 1
          when /^=begin\b/
            in_comment_block = false
            comment_lines += 1
          when /^__END__$/
            in_comment_block = true
          end
        end
      end

      codelines = lines - comment_lines - empty_lines

      stats[:lines] += lines
      stats[:comments] += comment_lines
      stats[:codelines] += codelines
      stats[:classes] += classes
      stats[:modules] += modules
      stats[:methods] += methods
      stats[:files] += 1
    end

    stats
  end

  def calculate_total
    total = Hash.new 0
    @statistics.each_value { |pair| pair.each { |k, v| total[k] += v } }
    total
  end

  def calculate_code
    code_loc = 0
    @statistics.each { |k, v| code_loc += v[:codelines] unless k[TEST_TYPES] }
    code_loc
  end

  def calculate_tests
    test_loc = 0
    @statistics.each { |k, v| test_loc += v[:codelines] if k[TEST_TYPES] }
    test_loc
  end

  def print_header
    print_splitter
    puts "| T=Test  Name              | Files | Lines |   LOC | Comments | Classes | Modules | Methods | M/C | LOC/M |"
    print_splitter
  end

  def print_splitter
    puts "+---------------------------+-------+-------+-------+----------+---------+---------+---------+-----+-------+"
  end

  def print_line name, statistics
    m_over_c = (statistics[:methods] / (statistics[:classes] + statistics[:modules])) rescue m_over_c = 0
    loc_over_m = (statistics[:codelines] / statistics[:methods]) - 2 rescue loc_over_m = 0

    if name[TEST_TYPES]
      name = "T #{name}"
    else
      name = "  #{name}"
    end

    line = "| %-25s | %5d | %5d | %5d | %8d | %7d | %7d | %7d | %3d | %5d |" % (
      [name, *statistics.values_at(:files, :lines, :codelines, :comments, :classes, :modules, :methods)] +
      [m_over_c, loc_over_m] )

    puts line
  end

  def print_code_test_stats
    code = calculate_code
    tests = calculate_tests

    puts "  Code LOC = #{code}     Test LOC = #{tests}     Code:Test Ratio = [1 : #{sprintf("%.2f", tests.to_f/code)}]"
    puts ""
  end

end

# Run a test script.
if $0 == __FILE__
  $VERBOSE = true
  CodeStatistics.new(
    ['This dir', File.dirname(__FILE__)]
  ).print
end