diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 2d197e21ea..fc362837d6 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,5 +1,10 @@ ## Rails 4.0.0 (unreleased) ## +* Improve `rake stats` for JavaScript and CoffeeScript: ignore block comments + and calculates number of functions. + + *Hendy Tanata* + * Ability to use a custom builder by passing `--builder` (or `-b`) has been removed. Consider using application template instead. See this guide for more detail: http://guides.rubyonrails.org/rails_application_templates.html diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb index 039360fcf6..0ae6d2a455 100644 --- a/railties/lib/rails/code_statistics.rb +++ b/railties/lib/rails/code_statistics.rb @@ -1,3 +1,5 @@ +require 'rails/code_statistics_calculator' + class CodeStatistics #:nodoc: TEST_TYPES = ['Controller tests', @@ -33,64 +35,38 @@ class CodeStatistics #:nodoc: end def calculate_directory_statistics(directory, pattern = /.*\.(rb|js|coffee)$/) - stats = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0 } + stats = CodeStatisticsCalculator.new Dir.foreach(directory) do |file_name| - if File.directory?(directory + "/" + file_name) and (/^\./ !~ file_name) - newstats = calculate_directory_statistics(directory + "/" + file_name, pattern) - stats.each { |k, v| stats[k] += newstats[k] } + path = "#{directory}/#{file_name}" + + if File.directory?(path) && (/^\./ !~ file_name) + stats.add(calculate_directory_statistics(path, pattern)) end next unless file_name =~ pattern - comment_started = false - - case file_name - when /.*\.js$/ - comment_pattern = /^\s*\/\// - else - comment_pattern = /^\s*#/ - end - - File.open(directory + "/" + file_name) do |f| - while line = f.gets - stats["lines"] += 1 - if(comment_started) - if line =~ /^=end/ - comment_started = false - end - next - else - if line =~ /^=begin/ - comment_started = true - next - end - end - stats["classes"] += 1 if line =~ /^\s*class\s+[_A-Z]/ - stats["methods"] += 1 if line =~ /^\s*def\s+[_a-z]/ - stats["codelines"] += 1 unless line =~ /^\s*$/ || line =~ comment_pattern - end - end + stats.add_by_file_path(path) end stats end def calculate_total - total = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0 } - @statistics.each_value { |pair| pair.each { |k, v| total[k] += v } } - 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['codelines'] unless TEST_TYPES.include? k } + @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['codelines'] if TEST_TYPES.include? k } + @statistics.each { |k, v| test_loc += v.code_lines if TEST_TYPES.include? k } test_loc end @@ -105,15 +81,15 @@ class CodeStatistics #:nodoc: end def print_line(name, statistics) - m_over_c = (statistics["methods"] / statistics["classes"]) rescue m_over_c = 0 - loc_over_m = (statistics["codelines"] / statistics["methods"]) - 2 rescue loc_over_m = 0 + 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 - puts "| #{name.ljust(20)} " + - "| #{statistics["lines"].to_s.rjust(5)} " + - "| #{statistics["codelines"].to_s.rjust(5)} " + - "| #{statistics["classes"].to_s.rjust(7)} " + - "| #{statistics["methods"].to_s.rjust(7)} " + - "| #{m_over_c.to_s.rjust(3)} " + + puts "| #{name.ljust(20)} " \ + "| #{statistics.lines.to_s.rjust(5)} " \ + "| #{statistics.code_lines.to_s.rjust(5)} " \ + "| #{statistics.classes.to_s.rjust(7)} " \ + "| #{statistics.methods.to_s.rjust(7)} " \ + "| #{m_over_c.to_s.rjust(3)} " \ "| #{loc_over_m.to_s.rjust(5)} |" end diff --git a/railties/lib/rails/code_statistics_calculator.rb b/railties/lib/rails/code_statistics_calculator.rb new file mode 100644 index 0000000000..60e4aef9b7 --- /dev/null +++ b/railties/lib/rails/code_statistics_calculator.rb @@ -0,0 +1,79 @@ +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: /[-=]>/, + } + } + + 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| + self.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) + File.extname(file_path).sub(/\A\./, '').downcase.to_sym + end +end diff --git a/railties/test/code_statistics_calculator_test.rb b/railties/test/code_statistics_calculator_test.rb new file mode 100644 index 0000000000..b3eabf5024 --- /dev/null +++ b/railties/test/code_statistics_calculator_test.rb @@ -0,0 +1,288 @@ +require 'abstract_unit' +require 'rails/code_statistics_calculator' + +class CodeStatisticsCalculatorTest < ActiveSupport::TestCase + def setup + @code_statistics_calculator = CodeStatisticsCalculator.new + end + + test 'add statistics to another using #add' do + code_statistics_calculator_1 = CodeStatisticsCalculator.new(1, 2, 3, 4) + @code_statistics_calculator.add(code_statistics_calculator_1) + + assert_equal 1, @code_statistics_calculator.lines + assert_equal 2, @code_statistics_calculator.code_lines + assert_equal 3, @code_statistics_calculator.classes + assert_equal 4, @code_statistics_calculator.methods + + code_statistics_calculator_2 = CodeStatisticsCalculator.new(2, 3, 4, 5) + @code_statistics_calculator.add(code_statistics_calculator_2) + + assert_equal 3, @code_statistics_calculator.lines + assert_equal 5, @code_statistics_calculator.code_lines + assert_equal 7, @code_statistics_calculator.classes + assert_equal 9, @code_statistics_calculator.methods + end + + test 'accumulate statistics using #add_by_io' do + code_statistics_calculator_1 = CodeStatisticsCalculator.new(1, 2, 3, 4) + @code_statistics_calculator.add(code_statistics_calculator_1) + + code = <<-'CODE' + def foo + puts 'foo' + end + + def bar; end + class A; end + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :rb) + + assert_equal 7, @code_statistics_calculator.lines + assert_equal 7, @code_statistics_calculator.code_lines + assert_equal 4, @code_statistics_calculator.classes + assert_equal 6, @code_statistics_calculator.methods + end + + test 'calculate statistics using #add_by_file_path' do + tmp_path = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'tmp')) + FileUtils.mkdir_p(tmp_path) + + code = <<-'CODE' + def foo + puts 'foo' + # bar + end + CODE + + file_path = "#{tmp_path}/stats.rb" + File.open(file_path, 'w') { |f| f.write(code) } + + @code_statistics_calculator.add_by_file_path(file_path) + + assert_equal 4, @code_statistics_calculator.lines + assert_equal 3, @code_statistics_calculator.code_lines + assert_equal 0, @code_statistics_calculator.classes + assert_equal 1, @code_statistics_calculator.methods + + FileUtils.rm_rf(tmp_path) + end + + test 'calculate number of Ruby methods' do + code = <<-'CODE' + def foo + puts 'foo' + end + + def bar; end + + class Foo + def bar(abc) + end + end + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :rb) + + assert_equal 3, @code_statistics_calculator.methods + end + + test 'calculate Ruby LOCs' do + code = <<-'CODE' + def foo + puts 'foo' + end + + # def bar; end + + class A < B + end + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :rb) + + assert_equal 8, @code_statistics_calculator.lines + assert_equal 5, @code_statistics_calculator.code_lines + end + + test 'calculate number of Ruby classes' do + code = <<-'CODE' + class Foo < Bar + def foo + puts 'foo' + end + end + + class Z; end + + # class A + # end + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :rb) + + assert_equal 2, @code_statistics_calculator.classes + end + + test 'skip Ruby comments' do + code = <<-'CODE' +=begin + class Foo + def foo + puts 'foo' + end + end +=end + + # class A + # end + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :rb) + + assert_equal 10, @code_statistics_calculator.lines + assert_equal 0, @code_statistics_calculator.code_lines + assert_equal 0, @code_statistics_calculator.classes + assert_equal 0, @code_statistics_calculator.methods + end + + test 'calculate number of JS methods' do + code = <<-'CODE' + function foo(x, y, z) { + doX(); + } + + $(function () { + bar(); + }) + + var baz = function ( x ) { + } + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :js) + + assert_equal 3, @code_statistics_calculator.methods + end + + test 'calculate JS LOCs' do + code = <<-'CODE' + function foo() + alert('foo'); + end + + // var b = 2; + + var a = 1; + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :js) + + assert_equal 7, @code_statistics_calculator.lines + assert_equal 4, @code_statistics_calculator.code_lines + end + + test 'skip JS comments' do + code = <<-'CODE' + /* + * var f = function () { + 1 / 2; + } + */ + + // call(); + // + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :js) + + assert_equal 8, @code_statistics_calculator.lines + assert_equal 0, @code_statistics_calculator.code_lines + assert_equal 0, @code_statistics_calculator.classes + assert_equal 0, @code_statistics_calculator.methods + end + + test 'calculate number of CoffeeScript methods' do + code = <<-'CODE' + square = (x) -> x * x + + math = + cube: (x) -> x * square x + + fill = (container, liquid = "coffee") -> + "Filling the #{container} with #{liquid}..." + + $('.shopping_cart').bind 'click', (event) => + @customer.purchase @cart + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :coffee) + + assert_equal 4, @code_statistics_calculator.methods + end + + test 'calculate CoffeeScript LOCs' do + code = <<-'CODE' + # Assignment: + number = 42 + opposite = true + + ### + CoffeeScript Compiler v1.4.0 + Released under the MIT License + ### + + # Conditions: + number = -42 if opposite + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :coffee) + + assert_equal 11, @code_statistics_calculator.lines + assert_equal 3, @code_statistics_calculator.code_lines + end + + test 'calculate number of CoffeeScript classes' do + code = <<-'CODE' + class Animal + constructor: (@name) -> + + move: (meters) -> + alert @name + " moved #{meters}m." + + class Snake extends Animal + move: -> + alert "Slithering..." + super 5 + + # class Horse + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :coffee) + + assert_equal 2, @code_statistics_calculator.classes + end + + test 'skip CoffeeScript comments' do + code = <<-'CODE' +### +class Animal + constructor: (@name) -> + + move: (meters) -> + alert @name + " moved #{meters}m." + ### + + # class Horse + alert 'hello' + CODE + + @code_statistics_calculator.add_by_io(StringIO.new(code), :coffee) + + assert_equal 10, @code_statistics_calculator.lines + assert_equal 1, @code_statistics_calculator.code_lines + assert_equal 0, @code_statistics_calculator.classes + assert_equal 0, @code_statistics_calculator.methods + end +end