code-stats 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +16 -0
- data/bin/code_stats +31 -0
- data/lib/code_stats.rb +28 -0
- data/lib/code_stats/code_stats.rb +51 -0
- data/lib/code_stats/file_set.rb +43 -0
- data/lib/code_stats/gems.rb +6 -0
- data/lib/code_stats/languages.rb +134 -0
- data/lib/code_stats/project.rb +55 -0
- data/lib/code_stats/report.html.haml +59 -0
- data/lib/code_stats/report.rb +42 -0
- data/lib/code_stats/report/highchart.js +11103 -0
- data/lib/code_stats/report/jquery.js +8936 -0
- data/lib/code_stats/report/report.js +75 -0
- data/lib/code_stats/report/style.css +14 -0
- data/lib/code_stats/support.rb +13 -0
- data/readme.md +40 -0
- data/spec/code_stats_spec.rb +15 -0
- data/spec/languages_spec.rb +221 -0
- data/spec/project_spec.rb +35 -0
- data/spec/sample_project/docs/sample.rb +1 -0
- data/spec/sample_project/specs/a_spec.rb +2 -0
- data/spec/sample_project/src/a.js +10 -0
- data/spec/sample_project/src/b.rb +8 -0
- data/spec/sample_project/src/c.unknown +0 -0
- data/spec/spec_helper.rb +8 -0
- metadata +123 -0
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rake_ext'
|
2
|
+
|
3
|
+
project(
|
4
|
+
name: "code_stats",
|
5
|
+
official_name: 'code-stats',
|
6
|
+
gem: true,
|
7
|
+
summary: "Language-agnostic Code Statistics",
|
8
|
+
|
9
|
+
bin: 'bin',
|
10
|
+
executables: ['code_stats'],
|
11
|
+
dirs: %w(bin),
|
12
|
+
version: '0.1.1',
|
13
|
+
|
14
|
+
author: "Alexey Petrushin",
|
15
|
+
homepage: "http://github.com/alexeypetrushin/misc/code_stats"
|
16
|
+
)
|
data/bin/code_stats
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
lib_dir = %(#{File.expand_path "#{__FILE__}/../.."}/lib)
|
4
|
+
$LOAD_PATH << lib_dir unless $LOAD_PATH.include? lib_dir
|
5
|
+
|
6
|
+
require 'code_stats'
|
7
|
+
|
8
|
+
if ARGV.empty? or %w(-h --help help).any?{|k| ARGV.include? k}
|
9
|
+
puts <<-TEXT
|
10
|
+
Language-agnostic Code Statistics
|
11
|
+
|
12
|
+
Usage:
|
13
|
+
$ code_stats /projects/wordpress /projects/drupal
|
14
|
+
$ code_stats /projects/*
|
15
|
+
$ code_stats /projects/wordpress /projects/drupal except: JavaScript skip_filter: /tmp/
|
16
|
+
|
17
|
+
Options:
|
18
|
+
except: JavaScript - analyze all languages except JavaScript
|
19
|
+
only: Ruby - analyze Ruby language only
|
20
|
+
specs_filter: regex, specifies should be path threated as specs, default: #{CodeStats::Project::DEFAUL_SPEC_FILTER.source}
|
21
|
+
skip_filter: regex, specifies paths that should be skipped, default: #{CodeStats::Project::DEFAUL_SKIP_FILTER.source}
|
22
|
+
TEXT
|
23
|
+
exit
|
24
|
+
end
|
25
|
+
|
26
|
+
args = RubyExt.argv
|
27
|
+
options = args.extract_options!
|
28
|
+
options2 = {}; options.each{|k, v| options2[k] = v ? v.to_sym : v}
|
29
|
+
args << options2
|
30
|
+
|
31
|
+
CodeStats.analyze_and_report *args
|
data/lib/code_stats.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
raise 'ruby 1.9.2 or higher required!' if RUBY_VERSION < '1.9.2'
|
2
|
+
|
3
|
+
require 'code_stats/gems'
|
4
|
+
|
5
|
+
require 'ruby_ext'
|
6
|
+
require 'vfs'
|
7
|
+
require 'haml'
|
8
|
+
require 'tilt'
|
9
|
+
|
10
|
+
class CodeStats
|
11
|
+
class << self
|
12
|
+
def extensions; @extensions ||= {} end
|
13
|
+
|
14
|
+
attr_accessor :file_size_limit
|
15
|
+
attr_required :file_size_limit
|
16
|
+
end
|
17
|
+
|
18
|
+
self.file_size_limit = 500 * 1024
|
19
|
+
end
|
20
|
+
|
21
|
+
%w(
|
22
|
+
support
|
23
|
+
languages
|
24
|
+
file_set
|
25
|
+
project
|
26
|
+
report
|
27
|
+
code_stats
|
28
|
+
).each{|f| require "code_stats/#{f}"}
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class CodeStats
|
2
|
+
class << self
|
3
|
+
def know? extension
|
4
|
+
extensions.include? extension
|
5
|
+
end
|
6
|
+
|
7
|
+
def parse text, extension
|
8
|
+
language = extensions[extension] || raise("no language for :#{extension} extension!")
|
9
|
+
language.new text
|
10
|
+
end
|
11
|
+
|
12
|
+
def analyze *args
|
13
|
+
options = args.extract_options!
|
14
|
+
paths = args
|
15
|
+
paths.collect!{|path| Dir[path]}.flatten!
|
16
|
+
|
17
|
+
puts <<-TEXT
|
18
|
+
please wait, analyzing:
|
19
|
+
#{paths.join("\n ")}
|
20
|
+
TEXT
|
21
|
+
|
22
|
+
projects = paths.collect do |path|
|
23
|
+
project = Project.new path, options
|
24
|
+
project.analyze!
|
25
|
+
project
|
26
|
+
end
|
27
|
+
projects.sort{|a, b| b.characters_count <=> a.characters_count}
|
28
|
+
end
|
29
|
+
|
30
|
+
def analyze_and_report *args
|
31
|
+
# parsing options
|
32
|
+
options = args.extract_options!
|
33
|
+
paths = args
|
34
|
+
|
35
|
+
options.validate_options! *(FileSet::AVAILIABLE_OPTIONS + Project::AVAILIABLE_OPTIONS)
|
36
|
+
project_options = options.select{|k, v| Project::AVAILIABLE_OPTIONS.include? k}
|
37
|
+
lang_options = options.select{|k, v| FileSet::AVAILIABLE_OPTIONS.include? k}
|
38
|
+
|
39
|
+
# analyzing & reporting
|
40
|
+
projects = analyze *(paths << project_options)
|
41
|
+
report = Report.new(*(projects << lang_options)).render
|
42
|
+
|
43
|
+
report_file = "./projects_statistics.html".to_file
|
44
|
+
report_file.write! report
|
45
|
+
|
46
|
+
puts "done, statistics are in #{report_file}"
|
47
|
+
|
48
|
+
Kernel.exec "open #{report_file}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class CodeStats::FileSet
|
2
|
+
def initialize
|
3
|
+
@lines_count_by_language, @characters_count_by_language = Hash.new(0), Hash.new(0)
|
4
|
+
end
|
5
|
+
|
6
|
+
def lines_count options = {}
|
7
|
+
total_count = 0
|
8
|
+
lines_count_by_language(options).each{|lang, count| total_count += count}
|
9
|
+
total_count
|
10
|
+
end
|
11
|
+
|
12
|
+
def characters_count options = {}
|
13
|
+
total_count = 0
|
14
|
+
characters_count_by_language(options).each{|lang, count| total_count += count}
|
15
|
+
total_count
|
16
|
+
end
|
17
|
+
|
18
|
+
def lines_count_by_language options = {}
|
19
|
+
filter_by_lang(@lines_count_by_language, options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def characters_count_by_language options = {}
|
23
|
+
filter_by_lang(@characters_count_by_language, options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def add script
|
27
|
+
@lines_count_by_language[script.class.alias.to_sym] += script.lines_count
|
28
|
+
@characters_count_by_language[script.class.alias.to_sym] += script.characters_count
|
29
|
+
end
|
30
|
+
|
31
|
+
AVAILIABLE_OPTIONS = [:only, :except]
|
32
|
+
|
33
|
+
protected
|
34
|
+
def filter_by_lang languages, options
|
35
|
+
options.validate_options! *AVAILIABLE_OPTIONS
|
36
|
+
only = options[:only] && Array.wrap(options[:only]).collect{|v| v.to_sym}
|
37
|
+
except = options[:except] && Array.wrap(options[:except]).collect{|v| v.to_sym}
|
38
|
+
|
39
|
+
languages.reject do |lang, count|
|
40
|
+
(only && !only.include?(lang)) or (except and except.include?(lang))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module CodeStats::Languages
|
2
|
+
class Abstract
|
3
|
+
attr_reader :text
|
4
|
+
|
5
|
+
def initialize text
|
6
|
+
@text = text
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.extensions *args
|
10
|
+
if args.size > 0
|
11
|
+
@extensions = args
|
12
|
+
|
13
|
+
@extensions.each{|ext| CodeStats.extensions[ext] = self}
|
14
|
+
else
|
15
|
+
@extensions ||= []
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def lines_count
|
20
|
+
calculate_basic_statistics
|
21
|
+
@lines_count
|
22
|
+
end
|
23
|
+
|
24
|
+
def characters_count
|
25
|
+
calculate_basic_statistics
|
26
|
+
@characters_count
|
27
|
+
end
|
28
|
+
|
29
|
+
def analyze
|
30
|
+
calculate_basic_statistics
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
def calculate_basic_statistics
|
35
|
+
return if @basic_statistics_calculated
|
36
|
+
@basic_statistics_calculated = true
|
37
|
+
|
38
|
+
@lines_count = code.blank? ? 0 : 1
|
39
|
+
@characters_count = 0
|
40
|
+
code.chars do |c|
|
41
|
+
@lines_count += 1 if c == "\n"
|
42
|
+
@characters_count += 1 unless c =~ /\s/
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module CComments
|
48
|
+
def code
|
49
|
+
@code ||= text.substitute(/\/\*.+?\*\/\n?/m, '').substitute(/^\/\/.+?[\n\z]/m, '')
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module SharpComments
|
54
|
+
def code
|
55
|
+
@code ||= text.substitute(/^#.+?[\n\z]/m, '')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Java < Abstract
|
60
|
+
include CComments
|
61
|
+
|
62
|
+
extensions :java
|
63
|
+
end
|
64
|
+
|
65
|
+
class JavaScript < Abstract
|
66
|
+
include CComments
|
67
|
+
|
68
|
+
extensions :js
|
69
|
+
end
|
70
|
+
|
71
|
+
class Ruby < Abstract
|
72
|
+
include SharpComments
|
73
|
+
|
74
|
+
extensions :rb
|
75
|
+
end
|
76
|
+
|
77
|
+
class Cpp < Abstract
|
78
|
+
include CComments
|
79
|
+
|
80
|
+
extensions :c, :cpp, :h
|
81
|
+
end
|
82
|
+
|
83
|
+
class Yaml < Abstract
|
84
|
+
include SharpComments
|
85
|
+
|
86
|
+
extensions :yml
|
87
|
+
end
|
88
|
+
|
89
|
+
class CoffeeScript < Abstract
|
90
|
+
include SharpComments
|
91
|
+
|
92
|
+
extensions :coffee
|
93
|
+
|
94
|
+
def code
|
95
|
+
@code ||= text.substitute(/###.+?###\n?/m, '').substitute(/^#.+?[\n\z]/m, '')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class Haml < Abstract
|
100
|
+
extensions :haml
|
101
|
+
|
102
|
+
def code
|
103
|
+
@code ||= text.substitute(/^\/.+?[\n\z]/m, '')
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class Erb < Abstract
|
108
|
+
extensions :erb
|
109
|
+
|
110
|
+
def code
|
111
|
+
@code ||= text
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class Rjs < Abstract
|
116
|
+
extensions :rjs
|
117
|
+
|
118
|
+
def code
|
119
|
+
@code ||= text
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class Php < Abstract
|
124
|
+
include CComments
|
125
|
+
|
126
|
+
extensions :php
|
127
|
+
end
|
128
|
+
|
129
|
+
class Python < Abstract
|
130
|
+
include SharpComments
|
131
|
+
|
132
|
+
extensions :py
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
class CodeStats::Project
|
2
|
+
attr_reader :path, :name, :specs_filter, :skip_filter, :unknown_extensions
|
3
|
+
attr_reader :sources, :specs
|
4
|
+
|
5
|
+
AVAILIABLE_OPTIONS = [:specs_filter, :skip_filter]
|
6
|
+
DEFAUL_SPEC_FILTER = /\/tests?\/|\/specs?\//
|
7
|
+
DEFAUL_SKIP_FILTER = /\/docs?\/|\/(s|ex)amples?\/|\/guides?\/|\/.git\/|\/tmp\/|database\.php/
|
8
|
+
|
9
|
+
def initialize path, options = {}
|
10
|
+
@path, @name = path, path.to_entry.name
|
11
|
+
options.validate_options! *AVAILIABLE_OPTIONS
|
12
|
+
@specs_filter = options.include?(:specs_filter) ? options[:specs_filter] : DEFAUL_SPEC_FILTER
|
13
|
+
@skip_filter = options.include?(:skip_filter) ? options[:skip_filter] : DEFAUL_SKIP_FILTER
|
14
|
+
clear
|
15
|
+
end
|
16
|
+
|
17
|
+
def files &b
|
18
|
+
files = []
|
19
|
+
path.to_dir.files '**/*.*' do |file|
|
20
|
+
next if skip?(file)
|
21
|
+
b ? b.call(file) : files.push(file)
|
22
|
+
end
|
23
|
+
b ? nil : files
|
24
|
+
end
|
25
|
+
|
26
|
+
delegate :lines_count, :characters_count, :lines_count_by_language, :characters_count_by_language, to: :sources
|
27
|
+
|
28
|
+
def analyze!
|
29
|
+
clear
|
30
|
+
files do |file|
|
31
|
+
ext = file.extension.to_sym
|
32
|
+
if CodeStats.know?(ext)
|
33
|
+
warn("file #{file} is too big, skipping") and next if file.size > CodeStats.file_size_limit
|
34
|
+
script = CodeStats.parse file.read.force_utf8_encoding, ext
|
35
|
+
source?(file) ? sources.add(script) : specs.add(script)
|
36
|
+
else
|
37
|
+
unknown_extensions << ext unless unknown_extensions.include? ext
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
def clear
|
44
|
+
@unknown_extensions = []
|
45
|
+
@sources, @specs = CodeStats::FileSet.new, CodeStats::FileSet.new
|
46
|
+
end
|
47
|
+
|
48
|
+
def source? file
|
49
|
+
!(specs_filter && (file.path.sub(path, '') =~ specs_filter))
|
50
|
+
end
|
51
|
+
|
52
|
+
def skip? file
|
53
|
+
skip_filter && (file.path.sub(path, '') =~ skip_filter)
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
!!! 5
|
2
|
+
%html
|
3
|
+
%head
|
4
|
+
%style{type: 'text/css'}
|
5
|
+
= css
|
6
|
+
|
7
|
+
/ Ignore all this JS stuff, it's just some JS libraries explicitly embedded into single HTML file
|
8
|
+
%script{type: 'text/javascript'}
|
9
|
+
= js
|
10
|
+
%body
|
11
|
+
- print_by_lang = -> counts {counts.to_a.sort{|a, b| b[1] <=> a[1]}.collect{|lang, count| "#{lang}: #{count.to_s_with_delimiter}"}.join('<br/>')}
|
12
|
+
|
13
|
+
%h1 Projects Statistics
|
14
|
+
|
15
|
+
#plot.plot
|
16
|
+
|
17
|
+
%h1 Details
|
18
|
+
|
19
|
+
%script{type: 'text/javascript'}
|
20
|
+
- projects_js = projects.collect{|p| "['#{p.name}', #{p.characters_count}, #{p.specs.characters_count}]"}.join(",\n")
|
21
|
+
Report.plot('#chart', [
|
22
|
+
= projects_js
|
23
|
+
]);
|
24
|
+
|
25
|
+
%table.report
|
26
|
+
%thead
|
27
|
+
%tr
|
28
|
+
%th Name
|
29
|
+
|
30
|
+
%th Chars
|
31
|
+
%th By Lang
|
32
|
+
- unless filters.empty?
|
33
|
+
%th Ignored
|
34
|
+
|
35
|
+
%th Spec Chars
|
36
|
+
%th Spec by Lang
|
37
|
+
- unless filters.empty?
|
38
|
+
%th Spec Ignored
|
39
|
+
|
40
|
+
%th Unknown
|
41
|
+
%tbody
|
42
|
+
- projects.each do |p|
|
43
|
+
%tr
|
44
|
+
%td= p.name
|
45
|
+
|
46
|
+
%td.nowrap.bold= p.characters_count.to_s_with_delimiter
|
47
|
+
%td.nowrap= print_by_lang.call p.characters_count_by_language
|
48
|
+
- unless filters.empty?
|
49
|
+
%td.nowrap= print_by_lang.call p.ignored_characters_count_by_language
|
50
|
+
|
51
|
+
%td.nowrap.bold= p.specs.characters_count.to_s_with_delimiter
|
52
|
+
%td.nowrap= print_by_lang.call p.specs.characters_count_by_language
|
53
|
+
- unless filters.empty?
|
54
|
+
%td.nowrap= print_by_lang.call p.specs.ignored_characters_count_by_language
|
55
|
+
|
56
|
+
%td= p.unknown_extensions.sort.join(', ')
|
57
|
+
|
58
|
+
%p.powered
|
59
|
+
Powered by <a href='https://github.com/alexeypetrushin/code_stats'>code_stat</a>
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class CodeStats::Report
|
2
|
+
attr_reader :filters, :projects
|
3
|
+
def initialize *args
|
4
|
+
@filters = args.extract_options!
|
5
|
+
@projects = args
|
6
|
+
end
|
7
|
+
|
8
|
+
def render
|
9
|
+
# preparing data
|
10
|
+
data = projects.clone
|
11
|
+
data.sort!{|a, b| b.characters_count(filters) <=> a.characters_count(filters)}
|
12
|
+
data.collect! do |p|
|
13
|
+
o = OpenObject.new
|
14
|
+
o.name = p.name
|
15
|
+
|
16
|
+
o.characters_count = p.characters_count(filters)
|
17
|
+
o.characters_count_by_language = p.characters_count_by_language(filters)
|
18
|
+
o.ignored_characters_count_by_language = p.characters_count_by_language.reject do |lang, count|
|
19
|
+
o.characters_count_by_language.include?(lang)
|
20
|
+
end
|
21
|
+
|
22
|
+
o.specs = OpenObject.new
|
23
|
+
o.specs.characters_count = p.specs.characters_count(filters)
|
24
|
+
o.specs.characters_count_by_language = p.specs.characters_count_by_language(filters)
|
25
|
+
o.specs.ignored_characters_count_by_language = p.specs.characters_count_by_language.reject do |lang, count|
|
26
|
+
o.specs.characters_count_by_language.include?(lang)
|
27
|
+
end
|
28
|
+
|
29
|
+
o.unknown_extensions = p.unknown_extensions
|
30
|
+
|
31
|
+
o
|
32
|
+
end
|
33
|
+
|
34
|
+
# rendering
|
35
|
+
dir = __FILE__.dirname.to_dir
|
36
|
+
css = [dir['report/style.css'].read].join("\n")
|
37
|
+
js = %w(jquery.js highchart.js report.js).collect{|n| dir["report/#{n}"].read}.join("\n")
|
38
|
+
|
39
|
+
report = dir / 'report.html.haml'
|
40
|
+
report.render(projects: data, filters: filters, css: css, js: js)
|
41
|
+
end
|
42
|
+
end
|