code-statistics 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-statistics',
|
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
|