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 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,6 @@
1
+ if respond_to? :fake_gem
2
+ fake_gem 'ruby_ext'
3
+ fake_gem 'vfs'
4
+ fake_gem 'haml'
5
+ fake_gem 'tilt'
6
+ 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