metric_adapter 0.0.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/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
+