metric_adapter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,58 @@
1
+ MetricAdapter
2
+ ==============
3
+
4
+ At [LiquidPlanner](http://liquidplanner.com), we wanted to build some tools to help us analyze our code. There are a [ton of static analyzers for ruby](http://xkcd.com/927/), each providing different insightful metrics, so there's no need to write a new analyzer. But what do you do when you want to use more than one of them?
5
+
6
+ Each analyzer takes a different configuration, and provides results in different formats. MetricAdapter solves the latter problem by converting the results of each analyzer into a common format.
7
+
8
+ Now you can take all of these wonderful libraries and combine their results without worrying about how they internally represent their metrics.
9
+
10
+ Metrics
11
+ -------
12
+ Each metric represents a single statement about one line of code. Flay for instance, generates reports that looks like this:
13
+
14
+ 1) IDENTICAL code found in :lasgn (mass*2 = 24)
15
+ lib/adapters/flay_adapter.rb:26
16
+ lib/adapters/flog_adapter.rb:23
17
+
18
+ In this case, two metrics would be generated, one per line. This makes it easier to annotate source files, or integrate these libraries with your favorite editor (`sed`, I kid, I know we all use `notepad` these days).
19
+
20
+ Each metric contains:
21
+
22
+ * Metric#location – An object representing a file path and line number
23
+ * Metric#signature – The associated method signature (_not all metrics currently have this_)
24
+ * Metric#message – A message explaining the metric, for instance "Has no descriptive comment"
25
+ * Metric#score – When applicable, a numeric score or rating indicating the severity
26
+
27
+ Examples
28
+ --------
29
+ To get normalized metrics, instantiate the static analysis library of your choice, and pass it to the appropriate adapter:
30
+
31
+ # Instantiate and configure your analyzer
32
+ flay = Flay.new :mass => 4
33
+ flay.process(*files)
34
+ flay.analyze
35
+
36
+ # Report on the adapted metrics
37
+ adapter = MetricAdapter::FlayAdapter.new(flay)
38
+ adapter.metrics.each do |m|
39
+ puts "#{m.location} - #{m.message}"
40
+ end
41
+
42
+ For a full example, see `examples/report.rb` and `examples/annotate.rb`. You can use these examples to build tools that are appropriate for your team.
43
+
44
+ Supported Libraries
45
+ -------------------
46
+
47
+ * Flay – Code duplication
48
+ * Flog – Code complexity
49
+ * Reek – Code smells
50
+
51
+
52
+ Contributing
53
+ ------------
54
+ Please do! Take a look at the existing adapters and tests (`lib/adapters` and `test/*_adapter_test.rb`) for examples.
55
+
56
+ ---
57
+
58
+ [Adam Sanderson](http://monkeyandcrow.com) for [LiquidPlanner](http://liquidplanner.com)
@@ -0,0 +1,34 @@
1
+ require 'flay'
2
+ require 'flog'
3
+ require 'reek'
4
+
5
+ # In your actual application, you would configure each of these static
6
+ # analyzers to match your own exacting standards. These mostly use the
7
+ # defaults for each library.
8
+ #
9
+ module AnalyzerMixin
10
+ # Generate metrics for Flay.
11
+ #
12
+ # In this sample, we are setting the `mass` option low so that there are more
13
+ # results.
14
+ #
15
+ def flay(files, mass = 4)
16
+ flay = Flay.new :mass => mass
17
+ flay.process(*files)
18
+ flay.analyze
19
+ MetricAdapter::FlayAdapter.new(flay).metrics
20
+ end
21
+
22
+ # Generate flog metrics.
23
+ def flog(files)
24
+ flog = Flog.new
25
+ flog.flog(*files)
26
+ MetricAdapter::FlogAdapter.new(flog).metrics
27
+ end
28
+
29
+ # Generate Reek metrics.
30
+ def reek(files)
31
+ examiner = Reek::Examiner.new(files)
32
+ MetricAdapter::ReekAdapter.new(examiner).metrics
33
+ end
34
+ end
@@ -0,0 +1,75 @@
1
+ DIR_NAME = File.dirname(__FILE__)
2
+ $:.unshift(DIR_NAME + '/../lib')
3
+
4
+ require "metric_adapter"
5
+ require "./#{DIR_NAME}/analyzer_mixin"
6
+
7
+ #
8
+ # This is an example of using MetricAdapter to provide a common
9
+ # interface for Flay, Flog, and Reek's metrics to annotate a file.
10
+ #
11
+
12
+ include AnalyzerMixin
13
+
14
+ COLORS = {
15
+ :normal => "\033[39m",
16
+ :alternate => "\033[38;5;99m",
17
+ :faded => "\033[90m"
18
+ }
19
+
20
+ files = ARGV.empty? ? [__FILE__] : ARGV
21
+
22
+ def report_on_file(path, metrics)
23
+ puts "\n#{path}"
24
+ return if !metrics || metrics.empty?
25
+
26
+ metrics_by_line = metrics.group_by{|m| m.location.line }
27
+
28
+ source = IO.read(path)
29
+ source.lines.each_with_index do |src_line, i|
30
+ line_number = i + 1 # Logical line numbers, not indices
31
+ src_line = src_line.chomp # Strip newline
32
+ indent = src_line.scan(/\A\s+/).first
33
+ line_metrics = metrics_by_line[line_number]
34
+
35
+ print_metrics(line_number, line_metrics, indent)
36
+ print_src(line_number, src_line)
37
+ end
38
+ end
39
+
40
+ def print_metrics(line_number, metrics, indent = "")
41
+ return unless metrics
42
+
43
+ metrics.each do |m|
44
+ print_line line_number, "#{indent}#{m.message}", :alternate
45
+ end
46
+ end
47
+
48
+ def print_src(line_number, src_line)
49
+ if src_line =~ /^\s*#/
50
+ # Print a comment
51
+ print_line line_number, src_line, :faded
52
+ else
53
+ # Print a normal line
54
+ print_line line_number, src_line, :normal
55
+ end
56
+ end
57
+
58
+ def print_line(line_number, text, name)
59
+ formatted_number = '%5d.' % line_number
60
+ puts "#{color formatted_number, :faded} #{color text, name}"
61
+ end
62
+
63
+ def color(text, name)
64
+ "#{COLORS[name]}#{text}\033[0m"
65
+ end
66
+
67
+ # Combine metrics from all our sources (See AnalyzerMixin):
68
+ metrics = flay(files, 16) + flog(files) + reek(files)
69
+
70
+ metrics_by_path = metrics.group_by{|m| m.location.path }
71
+
72
+ files.each do |path|
73
+ metrics = metrics_by_path[path]
74
+ report_on_file(path, metrics)
75
+ end
@@ -0,0 +1,37 @@
1
+ DIR_NAME = File.dirname(__FILE__)
2
+ $:.unshift(DIR_NAME + '/../lib')
3
+
4
+ require "metric_adapter"
5
+ require "./#{DIR_NAME}/analyzer_mixin"
6
+
7
+ include AnalyzerMixin
8
+
9
+ #
10
+ # This is a quick and dirty example of using MetricAdapter to provide a common
11
+ # interface for Flay, Flog, and Reek's metrics.
12
+ #
13
+
14
+ files = ARGV.empty? ? Dir[DIR_NAME + '/../lib/**/*.rb'] : ARGV
15
+
16
+ # Helper for displaying an ascii divider
17
+ def divider(char="-")
18
+ char * 80
19
+ end
20
+
21
+ # Combine metrics from all our sources (See AnalyzerMixin):
22
+ metrics = flay(files) + flog(files) + reek(files)
23
+
24
+ # Group those metrics by path
25
+ metrics_by_path = metrics.group_by{|m| m.location.path }
26
+
27
+ # For each path, print out a little report.
28
+ metrics_by_path.each do |path, metrics|
29
+ puts divider
30
+ puts path
31
+ puts divider
32
+
33
+ puts " line: message"
34
+ metrics.sort_by{|m| m.location }.each do |m|
35
+ puts "%6d: %s" % [m.location.line, m.message]
36
+ end
37
+ end
@@ -0,0 +1,51 @@
1
+ module MetricAdapter
2
+ class FlayAdapter
3
+ attr_reader :flay
4
+
5
+ def initialize(flay)
6
+ @flay = flay
7
+ end
8
+
9
+ def metrics
10
+ metrics = flay.hashes.map do |hash_key, nodes|
11
+ nodes.permutation(2).map do |node_a, node_b|
12
+ create_metric(hash_key, node_a, node_b)
13
+ end
14
+ end
15
+
16
+ metrics.flatten
17
+ end
18
+
19
+ private
20
+
21
+ def metrics_for_hash(hash_key, score)
22
+ nodes = flay.hashes[hash_key]
23
+ is_identical =
24
+
25
+ location = method_location(signature)
26
+ message = "Flog: #{score.round(2)}"
27
+
28
+ metric = Metric.new(location, signature, message)
29
+ metric.score = score
30
+
31
+ metric
32
+ end
33
+
34
+ def create_metric(hash_key, node_a, node_b)
35
+ is_identical = flay.identical[hash_key]
36
+ similarity = is_identical ? "identical" : "similar"
37
+ score = flay.masses[hash_key]
38
+
39
+ message = "Flay: #{score}, #{similarity} to #{node_location(node_b)}"
40
+ # TODO: generate signatures from locations
41
+ signature = ""
42
+
43
+ metric = Metric.new(node_location(node_a), signature, message)
44
+ end
45
+
46
+ def node_location(node)
47
+ Location.new(node.file, node.line)
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,47 @@
1
+ module MetricAdapter
2
+ class FlogAdapter
3
+ attr_reader :flog
4
+
5
+ def initialize(flog)
6
+ @flog = flog
7
+ end
8
+
9
+ def metrics
10
+ metrics = []
11
+
12
+ flog.each_by_score do |signature, score|
13
+ if locatable?(signature)
14
+ metrics << create_metric(signature, score)
15
+ end
16
+ end
17
+
18
+ metrics
19
+ end
20
+
21
+ private
22
+
23
+ def create_metric(signature, score)
24
+ location = method_location(signature)
25
+ message = "Flog: #{score.round(2)}"
26
+
27
+ metric = Metric.new(location, signature, message)
28
+ metric.score = score
29
+
30
+ metric
31
+ end
32
+
33
+ def method_location(signature)
34
+ path = flog.method_locations[signature]
35
+ Location.new(path)
36
+ end
37
+
38
+ # Flog will report on code that is not in a class or method,
39
+ # all of this top level code may be spread out across multiple
40
+ # files, so we can're report on that code's location.
41
+ def locatable?(signature)
42
+ !!flog.method_locations[signature]
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,32 @@
1
+ module MetricAdapter
2
+ class ReekAdapter
3
+ attr_reader :examiner
4
+
5
+ def initialize(examiner)
6
+ @examiner = examiner
7
+ end
8
+
9
+ def metrics
10
+ metrics = examiner.smells.map do |smell|
11
+ line_numbers = Array(smell.lines).uniq
12
+ line_numbers.map do |line|
13
+ create_metric(smell, line)
14
+ end
15
+ end
16
+
17
+ metrics.flatten
18
+ end
19
+
20
+ private
21
+
22
+ def create_metric(smell, line)
23
+ location = Location.new(smell.source, line)
24
+ message = "#{smell.message.capitalize} (#{smell.subclass})"
25
+ signature = smell.context
26
+
27
+ Metric.new(location, signature, message)
28
+ end
29
+
30
+ end
31
+
32
+ end
data/lib/location.rb ADDED
@@ -0,0 +1,36 @@
1
+ module MetricAdapter
2
+ class Location
3
+ attr_reader :path, :line
4
+
5
+ def initialize(combined_path, line = 0)
6
+ @path, @line = parse_path(combined_path || '')
7
+ @line ||= line
8
+ end
9
+
10
+ def to_s
11
+ "#{path}:#{line}"
12
+ end
13
+
14
+ def <=>(other)
15
+ [path,line] <=> [other.path, other.line]
16
+ end
17
+
18
+ private
19
+
20
+ def parse_path(path)
21
+ path = path.strip
22
+
23
+ if has_line_number?(path)
24
+ *path_chunks, line = path.split(':')
25
+ [path_chunks.join(':'), line.to_i]
26
+ else
27
+ [path,nil]
28
+ end
29
+ end
30
+
31
+ def has_line_number?(path)
32
+ !!(path =~ /\:\d+\s*$/)
33
+ end
34
+
35
+ end
36
+ end
data/lib/metric.rb ADDED
@@ -0,0 +1,29 @@
1
+ module MetricAdapter
2
+
3
+ # A normalized representation of a code metric.
4
+ class Metric
5
+
6
+ # Location where this metric applies
7
+ attr_accessor :location
8
+
9
+ # Associated class and method signature
10
+ attr_accessor :signature
11
+
12
+ # Message indicating the issue being reported
13
+ attr_accessor :message
14
+
15
+ # Optional score for for the metric indicating severity
16
+ # A score is not normalized across analyzers
17
+ attr_accessor :score
18
+
19
+ # Create an instance of Metric.
20
+ # `location` is expected to be a `Location` instance
21
+ def initialize(location, signature, message)
22
+ @location = location
23
+ @signature = signature
24
+ @message = message
25
+ @score = 0
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,8 @@
1
+ require 'metric'
2
+ require 'location'
3
+
4
+ module MetricAdapter
5
+ autoload :FlogAdapter, 'adapters/flog_adapter'
6
+ autoload :FlayAdapter, 'adapters/flay_adapter'
7
+ autoload :ReekAdapter, 'adapters/reek_adapter'
8
+ end
@@ -0,0 +1,53 @@
1
+ require "minitest/autorun"
2
+ require "metric_adapter"
3
+
4
+ require "flay"
5
+
6
+ class FlayAdapterTest < MiniTest::Unit::TestCase
7
+ include MetricAdapter
8
+
9
+ def test_empty_flay
10
+ flay = Flay.new
11
+ adapter = FlayAdapter.new(flay)
12
+
13
+ assert_equal [], adapter.metrics
14
+ end
15
+
16
+ def test_flay_adapts_locations
17
+ adapter = flay_self
18
+ flay = adapter.flay
19
+ # For each set of similar nodes, it should generate all the permutations (nodes choose 2)
20
+ expected_count = flay.hashes.map{|h, nodes| nodes ** 2}.inject{|n,m| n+m }
21
+
22
+ assert_equal expected_count, adapter.metrics.length, "Each set of similar nodes should generate n^2 metrics"
23
+ end
24
+
25
+ def test_flay_adapts_locations
26
+ adapter = flay_self
27
+ metric = adapter.metrics.first
28
+ location = metric.location
29
+
30
+ assert_equal __FILE__, location.path, "Should reference this file"
31
+ assert location.line != 0, "Line numbers should be captured (line:0 should not show up in flog)"
32
+ end
33
+
34
+ def test_flay_messages
35
+ adapter = flay_self
36
+ metric = adapter.metrics.first
37
+ message = metric.message
38
+
39
+ assert message =~ /flay/i, "Should reference flay: #{message.inspect}"
40
+ assert message =~ /#{Regexp.escape __FILE__}:\d+/, "Should reference the a path: #{message.inspect}"
41
+ end
42
+
43
+ private
44
+
45
+ def flay_self
46
+ flay = Flay.new :mass => 2
47
+ flay.process(__FILE__)
48
+ flay.analyze
49
+
50
+ FlayAdapter.new(flay)
51
+ end
52
+
53
+ end
@@ -0,0 +1,69 @@
1
+ require "minitest/autorun"
2
+ require "metric_adapter"
3
+
4
+ require "flog"
5
+
6
+ class FlogAdapterTest < MiniTest::Unit::TestCase
7
+ include MetricAdapter
8
+
9
+ def test_empty_flog
10
+ flog = Flog.new
11
+ adapter = FlogAdapter.new(flog)
12
+
13
+ assert_equal [], adapter.metrics
14
+ end
15
+
16
+ def test_flogging_adapts_locations
17
+ adapter = flog_self
18
+ metric = adapter.metrics.first
19
+ location = metric.location
20
+
21
+ assert_equal __FILE__, location.path, "Should reference this file"
22
+ assert location.line != 0, "Line numbers should be captured (line:0 should not show up in flog)"
23
+ end
24
+
25
+ def test_flogging_messages
26
+ adapter = flog_self
27
+ metric = adapter.metrics.first
28
+ message = metric.message
29
+
30
+ assert message =~ /flog/i, "Should reference flog: #{message.inspect}"
31
+ assert message =~ /\d+/, "Should reference the score: #{message.inspect}"
32
+ end
33
+
34
+ def test_ignoring_unlocatable_metrics
35
+ flog = Flog.new
36
+ flog.flog_ruby top_level_sample, "sample.rb"
37
+ flog.calculate_total_scores
38
+
39
+ adapter = FlogAdapter.new(flog)
40
+ metrics = adapter.metrics
41
+
42
+ assert metrics.empty?, "Flog should not attach location information to top level source"
43
+ end
44
+
45
+ private
46
+ def flog_self
47
+ flog = Flog.new
48
+ flog.flog(__FILE__)
49
+
50
+ FlogAdapter.new(flog)
51
+ end
52
+
53
+ def top_level_sample
54
+ <<-RUBY
55
+ # Some good meaningless floggable source outside the scope of a method
56
+ if xs.any?{|x| x ? true : false} && x.length > 3 && x.length < 9
57
+ xs.each do |x|
58
+ (x+1).times{|xp| report(xp / 2)}
59
+ yield x-1
60
+ end
61
+ if xs.length % 2 == 0
62
+ puts xs.length % 4 == 0 ? 1 : 0
63
+ end
64
+ end
65
+
66
+ puts xs.join(", ")
67
+ RUBY
68
+ end
69
+ end
@@ -0,0 +1,49 @@
1
+ require "minitest/autorun"
2
+ require "metric_adapter"
3
+
4
+ class LocationTest < MiniTest::Unit::TestCase
5
+ include MetricAdapter
6
+
7
+ def test_creating_a_location_with_line_number
8
+ path = "./test"
9
+ line = 7
10
+ location = Location.new(path, 7)
11
+
12
+ assert_equal path, location.path
13
+ assert_equal line, location.line
14
+ end
15
+
16
+ def test_creating_a_location_with_no_number
17
+ path = "./test"
18
+ location = Location.new(path)
19
+
20
+ assert_equal path, location.path
21
+ assert_equal 0, location.line
22
+ end
23
+
24
+ def test_creating_a_location_with_embedded_line_number
25
+ path = "./test"
26
+ line = 17
27
+ location = Location.new("#{path}:#{line}")
28
+
29
+ assert_equal path, location.path
30
+ assert_equal line, location.line
31
+ end
32
+
33
+ def test_retains_colons
34
+ path = "yes:we_have_no_banannas"
35
+ line = 17
36
+ location = Location.new("#{path}:#{line}")
37
+
38
+ assert_equal path, location.path
39
+ assert_equal line, location.line
40
+ end
41
+
42
+ def test_nil_path
43
+ location = Location.new(nil)
44
+
45
+ assert_equal "", location.path
46
+ assert_equal 0, location.line
47
+ end
48
+
49
+ end
@@ -0,0 +1,55 @@
1
+ require "minitest/autorun"
2
+ require "metric_adapter"
3
+
4
+ require "reek"
5
+
6
+ class ReekAdapterTest < MiniTest::Unit::TestCase
7
+ SAMPLE_PATH = 'dirty.rb'
8
+
9
+ include MetricAdapter
10
+
11
+ def test_empty_examiner
12
+ examiner = Reek::Examiner.new([])
13
+ adapter = ReekAdapter.new(examiner)
14
+
15
+ assert_equal [], adapter.metrics
16
+ end
17
+
18
+ def test_examiner_adapts_locations
19
+ adapter = examine_sample
20
+ metric = adapter.metrics.first
21
+ location = metric.location
22
+
23
+ assert_equal SAMPLE_PATH, location.path, "Should have included the path"
24
+ assert location.line > 0, "Should have a non zero line number"
25
+ end
26
+
27
+ def test_examiner_returns_metrics_for_each_line_of_each_smell
28
+ adapter = examine_sample
29
+ metrics = adapter.metrics
30
+ total_lines = adapter.examiner.smells.inject(0){|sum, s| sum + s.lines.length}
31
+
32
+ assert_equal total_lines, metrics.length, "Should have included a metric per line, per smell"
33
+ end
34
+
35
+ private
36
+
37
+ def examine_sample
38
+ examiner = Reek::Examiner.new(sample)
39
+ ReekAdapter.new(examiner)
40
+ end
41
+
42
+ def sample
43
+ Reek::Source::SourceCode.new(<<-SAMPLE, SAMPLE_PATH)
44
+ # Reek Sample Code
45
+ class Dirty
46
+ def a
47
+ puts @s.title
48
+ @s = fred.map {|x| x.each {|key| key += 3}}
49
+ puts @s.title
50
+ end
51
+ end
52
+ SAMPLE
53
+ end
54
+
55
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: metric_adapter
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Adam Sanderson
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2013-06-25 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: Provides a common interface for using metrics generated by tools such as Flog, Flay, and Reek.
22
+ email: netghost@gmail.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - README.markdown
29
+ files:
30
+ - lib/adapters/flay_adapter.rb
31
+ - lib/adapters/flog_adapter.rb
32
+ - lib/adapters/reek_adapter.rb
33
+ - lib/location.rb
34
+ - lib/metric.rb
35
+ - lib/metric_adapter.rb
36
+ - examples/analyzer_mixin.rb
37
+ - examples/annotate.rb
38
+ - examples/report.rb
39
+ - test/flay_adapter_test.rb
40
+ - test/flog_adapter_test.rb
41
+ - test/location_test.rb
42
+ - test/reek_adapter_test.rb
43
+ - README.markdown
44
+ has_rdoc: true
45
+ homepage: https://github.com/adamsanderson/metric_adapter
46
+ licenses:
47
+ - MIT
48
+ post_install_message:
49
+ rdoc_options:
50
+ - --main
51
+ - README.markdown
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ requirements: []
71
+
72
+ rubyforge_project:
73
+ rubygems_version: 1.3.7
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: Common interface for multiple static analyzers metrics.
77
+ test_files: []
78
+