courgette 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 57adf4c4137fcf9342cc36a2da2d18bae46adff3
4
+ data.tar.gz: c7d05925b78857a07c8765859e9c945dbcffd3e8
5
+ SHA512:
6
+ metadata.gz: 17e68370df6b304fdf7973b9f969e53d5bc9f30764a2eebdf0b33442b48bfddf6d4b40a81bcb4bd35679448e23a1962e222688339fc1797670d75943f732d5a4
7
+ data.tar.gz: bf96fc9d079926ea905030564e21a12f2543fbe546249056c53c85565deed8d05ac56e49b608e68d2fc3750009480a258506e7968aa8f5cd67aa5a6640e0ff2c
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.pattern = 'test/**/*.rb'
6
+ end
7
+
8
+ task :default => :test
data/bin/courgette.rb ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'commander/import'
5
+
6
+ require 'courgette/commands/graph'
7
+ require 'courgette/commands/stats'
8
+
9
+ program :version, '1.0.0'
10
+ program :description, 'Dependency graph fetcher'
11
+
12
+ default_command :stats
13
+
14
+ command :stats do |c|
15
+ c.syntax = 'courgette stats [options]'
16
+ c.summary = ''
17
+ c.description = ''
18
+ c.example 'description', 'command example'
19
+ c.option '--glob STRING', String, 'Glob to run metrics over (default: **/*.rb)'
20
+ c.action do |args, options|
21
+ options.default :glob => '**/*.rb'
22
+
23
+ Courgette::Commands::Stats.new(options).run
24
+ end
25
+ end
26
+
27
+ command :graph do |c|
28
+ c.syntax = 'courgette graph [options]'
29
+ c.summary = ''
30
+ c.description = ''
31
+ c.example 'description', 'command example'
32
+ c.option '--glob STRING', String, 'Glob to run metrics over (default: **/*.rb)'
33
+ c.option '--name STRING', String, 'Glob to run metrics over (default: graph)'
34
+ c.option '--format STRING', String, 'Format (default: png)'
35
+ c.action do |args, options|
36
+ options.default :glob => '**/*.rb', :format => 'png', :name => 'graph'
37
+
38
+ Courgette::Commands::Graph.new(options).run
39
+ end
40
+ end
data/courgette.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'courgette/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "courgette"
8
+ spec.version = Courgette::VERSION
9
+ spec.authors = ["Hugo Peixoto"]
10
+ spec.email = ["hugo.peixoto@gmail.com"]
11
+ spec.description = %q{Static ruby dependency analyser}
12
+ spec.summary = %q{Courgette analyses a set of files, and calculates a dependency graph}
13
+ spec.homepage = "https://github.com/hugopeixoto/courgette"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "ruby_parser"
22
+ spec.add_dependency "commander"
23
+ spec.add_dependency "graph"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.3"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "minitest"
28
+ end
@@ -0,0 +1,88 @@
1
+ require 'courgette/scope'
2
+
3
+ module Courgette
4
+ class Capturer
5
+ def initialize
6
+ @references = []
7
+ @definitions = []
8
+ end
9
+
10
+ def capture sexpr
11
+ iterate sexpr, []
12
+ end
13
+
14
+ def references
15
+ @references.uniq! { |reference| reference.to_a }
16
+ @references
17
+ end
18
+
19
+ def definitions
20
+ @definitions.uniq! { |definition| definition.to_a }
21
+ @definitions
22
+ end
23
+
24
+ private
25
+ Reference = Struct.new :name, :context
26
+
27
+ def add_definition definition
28
+ @definitions << definition.flatten
29
+ end
30
+
31
+ def add_reference context, reference
32
+ @references << Reference.new(reference, context)
33
+ end
34
+
35
+ def scope sexpr
36
+ Courgette::Scope.new.scope sexpr
37
+ end
38
+
39
+ def iterate_many sexprs, context
40
+ sexprs.each do |sexpr|
41
+ iterate sexpr, context
42
+ end
43
+ end
44
+
45
+ def iterate_cdecl sexpr, context
46
+ s = scope sexpr[1]
47
+ new_context = context + [s]
48
+ add_definition new_context
49
+
50
+ iterate_many sexpr[2..-1], context
51
+ end
52
+
53
+ def iterate_definition sexpr, context
54
+ s = scope sexpr[1]
55
+ new_context = context + [s]
56
+ add_definition new_context
57
+
58
+ iterate_many sexpr[2..-1], new_context
59
+ end
60
+
61
+ def iterate_call sexpr, context
62
+ iterate sexpr[1], context
63
+ iterate_many sexpr[3..-1], context
64
+ end
65
+
66
+
67
+ def iterate sexpr, context
68
+ return unless sexpr.is_a? Enumerable
69
+
70
+ case sexpr[0]
71
+ when :const, :colon2
72
+ add_reference context, scope(sexpr)
73
+ when :module, :class
74
+ iterate_definition sexpr, context
75
+ when :cdecl
76
+ iterate_cdecl sexpr, context
77
+ when :defn
78
+ iterate_many sexpr[3..-1], context
79
+
80
+ when :call
81
+ iterate_call sexpr, context
82
+ when :nil, :lit
83
+ else
84
+ iterate_many sexpr[1..-1], context
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,46 @@
1
+ require 'courgette/directory_analyser'
2
+ require 'courgette/graph2dot'
3
+
4
+ module Courgette
5
+ module Commands
6
+ class Graph
7
+ def initialize options
8
+ @options = options
9
+ end
10
+
11
+ def run
12
+ factory.load graph
13
+
14
+ formats.each do |fmt|
15
+ factory.save name, fmt
16
+ end
17
+ end
18
+
19
+ private
20
+ def glob
21
+ @options.glob
22
+ end
23
+
24
+ def formats
25
+ Array(@options.format)
26
+ end
27
+
28
+ def name
29
+ @options.name
30
+ end
31
+
32
+ def factory
33
+ @factory ||= Courgette::Graph2Dot.new do
34
+ boxes
35
+ node_attribs << filled
36
+ end
37
+ end
38
+
39
+ def graph
40
+ @graph ||= Courgette::DirectoryAnalyser.new.tap do |da|
41
+ da.analyse glob
42
+ end.graph
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,42 @@
1
+ require 'courgette/directory_analyser'
2
+
3
+ module Courgette
4
+ module Commands
5
+ class Stats
6
+ def initialize options
7
+ @options = options
8
+ end
9
+
10
+ def run
11
+ stats.each do |fanout, fanin, reference|
12
+ print_reference fanout, fanin, reference
13
+ end
14
+ end
15
+
16
+ def print_reference fo, fi, ref
17
+ print "%8d %8d: %s\n" % [fo, fi, ref.join("::")]
18
+ end
19
+
20
+ private
21
+ def glob
22
+ @options.glob
23
+ end
24
+
25
+ def stats
26
+ @stats ||= graph.nodes.map do |node|
27
+ [
28
+ graph.dependency_count(node),
29
+ graph.depender_count(node),
30
+ node
31
+ ]
32
+ end.sort
33
+ end
34
+
35
+ def graph
36
+ @graph ||= Courgette::DirectoryAnalyser.new.tap do |da|
37
+ da.analyse glob
38
+ end.graph
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,38 @@
1
+ require 'courgette/capturer'
2
+ require 'courgette/reference_to_dependency'
3
+ require 'courgette/file_to_sexpr'
4
+ require 'courgette/graph'
5
+
6
+ module Courgette
7
+ class DirectoryAnalyser
8
+ def initialize
9
+ @capturer = Courgette::Capturer.new
10
+ @file2sexpr = Courgette::FileToSexpr.new
11
+ end
12
+
13
+ def analyse pattern
14
+ Dir.glob(pattern) do |file|
15
+ capturer.capture file2sexpr.convert(file)
16
+ end
17
+ end
18
+
19
+ def graph
20
+ Courgette::Graph.new definitions, dependencies
21
+ end
22
+
23
+ private
24
+ attr_reader :capturer, :file2sexpr
25
+
26
+ def definitions
27
+ capturer.definitions
28
+ end
29
+
30
+ def dependencies
31
+ r2d = Courgette::ReferenceToDependency.new definitions
32
+
33
+ capturer.references.map do |reference|
34
+ r2d.transform reference
35
+ end.compact
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ require 'ruby_parser'
2
+
3
+ module Courgette
4
+ class FileToSexpr
5
+ def initialize
6
+ @parser = RubyParser.new
7
+ end
8
+
9
+ def convert filename
10
+ contents = File.read filename
11
+
12
+ @parser.parse contents
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ module Courgette
2
+ class Graph
3
+ attr_reader :nodes, :edges
4
+
5
+ def initialize nodes, edges
6
+ @nodes = nodes
7
+ @edges = edges
8
+ end
9
+
10
+ def dependency_count node
11
+ edges.select { |r| r.referrer == node }.count
12
+ end
13
+
14
+ def depender_count node
15
+ edges.select { |r| r.reference == node }.count
16
+ end
17
+
18
+ private
19
+ end
20
+ end
@@ -0,0 +1,56 @@
1
+ require 'courgette/node_grouper'
2
+ require 'graph'
3
+
4
+ module Courgette
5
+ class Graph2Dot
6
+ def initialize &block
7
+ @dot = digraph &block
8
+ end
9
+
10
+ def load graph
11
+ calculate_distinct_namespaces graph.nodes
12
+ set_colorscheme
13
+
14
+ graph.nodes.each do |node|
15
+ add_node node
16
+ end
17
+
18
+ graph.edges.each do |edge|
19
+ add_edge edge
20
+ end
21
+ end
22
+
23
+ def save *args
24
+ dot.save *args
25
+ end
26
+
27
+ private
28
+ attr_reader :dot
29
+
30
+ def set_colorscheme
31
+ dot.colorscheme(:set2, [4, [8, @grouper.groups.count + 1].min].max)
32
+ end
33
+
34
+ def calculate_distinct_namespaces nodes
35
+ @grouper = Courgette::NodeGrouper.new nodes, 8
36
+ end
37
+
38
+ def add_node node
39
+ color(node) << dot.node(label(node))
40
+ end
41
+
42
+ def add_edge edge
43
+ dot.edge(label(edge.referrer), label(edge.reference))
44
+ end
45
+
46
+ def color node
47
+ idx = @grouper.group node
48
+
49
+ dot.send "c#{idx}"
50
+ end
51
+
52
+ def label node
53
+ node.flatten.join("::")
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,27 @@
1
+ module Courgette
2
+ class NameResolution
3
+ def initialize definitions
4
+ @definitions = definitions
5
+ end
6
+
7
+ def resolve reference
8
+ definitions.find do |definition|
9
+ match_name?(definition, reference) &&
10
+ match_scope?(definition, reference)
11
+ end
12
+ end
13
+
14
+ private
15
+ attr_reader :definitions
16
+
17
+ def match_name? definition, reference
18
+ definition.last == reference.name.last
19
+ end
20
+
21
+ def match_scope? definition, reference
22
+ (0..reference.context.length).any? do |level|
23
+ reference.context[0...level].flatten + reference.name == definition
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,31 @@
1
+ module Courgette
2
+ class NodeGrouper
3
+ def initialize nodes, max_groups
4
+ @nodes = nodes
5
+ @max_groups = max_groups
6
+
7
+ calculate
8
+ end
9
+
10
+ def group node
11
+ idx = namespaces.index node[0...-1][0...1]
12
+ idx ||= -1
13
+ idx = [2 + idx, @max_groups].min
14
+ end
15
+
16
+ def groups
17
+ @namespaces
18
+ end
19
+
20
+ private
21
+ attr_reader :nodes, :namespaces
22
+
23
+ def calculate
24
+ @namespaces = nodes.
25
+ group_by { |n| n[0...-1][0...1] }.
26
+ reject { |k, v| k.empty? || v.length < 4 }.
27
+ map(&:first).
28
+ uniq
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ require 'courgette/name_resolution'
2
+
3
+ module Courgette
4
+ class ReferenceToDependency
5
+ def initialize definitions
6
+ @resolver = Courgette::NameResolution.new definitions
7
+ end
8
+
9
+ def transform reference
10
+ definition = @resolver.resolve reference
11
+ return if definition.nil?
12
+
13
+ Dependency.new definition, reference.context.flatten
14
+ end
15
+
16
+ private
17
+ attr_reader :resolver
18
+ Dependency = Struct.new :reference, :referrer
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ module Courgette
2
+ class Scope
3
+ def scope sexpr
4
+ case sexpr[0]
5
+ when :colon2
6
+ scope(sexpr[1]) + scope(sexpr[2])
7
+ when :const
8
+ scope sexpr[1]
9
+ else
10
+ [sexpr]
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,4 @@
1
+ module Courgette
2
+ VERSION = "0.1.0"
3
+ end
4
+
data/lib/courgette.rb ADDED
@@ -0,0 +1,2 @@
1
+ module Courgette
2
+ end
@@ -0,0 +1,72 @@
1
+ require 'minitest/autorun'
2
+ require 'courgette/name_resolution'
3
+
4
+ FakeReference = Struct.new :name, :context
5
+
6
+ class TestCourgetteNameResolution < MiniTest::Unit::TestCase
7
+ def create definitions
8
+ @resolver = Courgette::NameResolution.new definitions
9
+ end
10
+
11
+ def test_root_level
12
+ create [[:RootA], [:RootB]]
13
+
14
+ ref = FakeReference.new [:RootA], []
15
+
16
+ assert_equal [:RootA], @resolver.resolve(ref)
17
+ end
18
+
19
+ def test_root_unknown
20
+ create [[:Root]]
21
+
22
+ ref = FakeReference.new [:Unknown], []
23
+
24
+ assert_equal nil, @resolver.resolve(ref)
25
+ end
26
+
27
+ def test_top_definition_referenced_from_nested
28
+ create [[:Root]]
29
+
30
+ ref = FakeReference.new [:Root], [[:A, :B]]
31
+ assert_equal [:Root], @resolver.resolve(ref)
32
+ end
33
+
34
+ def test_nested_definition_referenced_from_nested
35
+ create [[:Scoped, :Name]]
36
+
37
+ ref = FakeReference.new [:Name], [[:Scoped]]
38
+ assert_equal [:Scoped, :Name], @resolver.resolve(ref)
39
+ end
40
+
41
+ def test_double_scope
42
+ definition = [:Potato, :Scoped, :Name]
43
+ create [definition]
44
+
45
+ ref = FakeReference.new [:Name], [[:Potato], [:Scoped]]
46
+ assert_equal definition, @resolver.resolve(ref)
47
+ end
48
+
49
+ def test_grouped_double_scope
50
+ definition = [:Potato, :Scoped, :Name]
51
+ create [definition]
52
+
53
+ ref = FakeReference.new [:Name], [[:Potato, :Scoped]]
54
+ assert_equal definition, @resolver.resolve(ref)
55
+ end
56
+
57
+ def test_scoped_definition_referenced_double
58
+ definition = [:Potato, :Name]
59
+ create [definition]
60
+
61
+ ref = FakeReference.new [:Name], [[:Potato], [:Scoped]]
62
+ assert_equal definition, @resolver.resolve(ref)
63
+ end
64
+
65
+ def test_scoped_definition_referenced_grouped
66
+ definition = [:Potato, :Name]
67
+ create [definition]
68
+
69
+ ref = FakeReference.new [:Name], [[:Potato, :Scoped]]
70
+ assert_equal nil, @resolver.resolve(ref)
71
+ end
72
+ end
@@ -0,0 +1,31 @@
1
+ require 'minitest/autorun'
2
+ require 'courgette/scope'
3
+
4
+ class TestCourgetteScope < MiniTest::Unit::TestCase
5
+ def setup
6
+ @scope = Courgette::Scope.new
7
+ end
8
+
9
+ def test_const
10
+ assert_equal [:Name], @scope.scope([:const, :Name])
11
+ end
12
+
13
+ def test_other
14
+ assert_equal [:potato], @scope.scope(:potato)
15
+ end
16
+
17
+ def test_colon2
18
+ assert_equal [:Scoped, :Name],
19
+ @scope.scope([:colon2, :Scoped, :Name])
20
+ end
21
+
22
+ def test_compose
23
+ assert_equal [:Really, :Scoped, :Name],
24
+ @scope.scope(
25
+ [:colon2,
26
+ [:colon2,
27
+ [:const, :Really],
28
+ [:const, :Scoped]],
29
+ [:const, :Name]])
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: courgette
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Hugo Peixoto
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ruby_parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: commander
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: graph
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Static ruby dependency analyser
98
+ email:
99
+ - hugo.peixoto@gmail.com
100
+ executables:
101
+ - courgette.rb
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - Gemfile
106
+ - Rakefile
107
+ - bin/courgette.rb
108
+ - courgette.gemspec
109
+ - lib/courgette.rb
110
+ - lib/courgette/capturer.rb
111
+ - lib/courgette/commands/graph.rb
112
+ - lib/courgette/commands/stats.rb
113
+ - lib/courgette/directory_analyser.rb
114
+ - lib/courgette/file_to_sexpr.rb
115
+ - lib/courgette/graph.rb
116
+ - lib/courgette/graph2dot.rb
117
+ - lib/courgette/name_resolution.rb
118
+ - lib/courgette/node_grouper.rb
119
+ - lib/courgette/reference_to_dependency.rb
120
+ - lib/courgette/scope.rb
121
+ - lib/courgette/version.rb
122
+ - test/courgette/test_name_resolution.rb
123
+ - test/courgette/test_scope.rb
124
+ homepage: https://github.com/hugopeixoto/courgette
125
+ licenses:
126
+ - MIT
127
+ metadata: {}
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubyforge_project:
144
+ rubygems_version: 2.0.3
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: Courgette analyses a set of files, and calculates a dependency graph
148
+ test_files:
149
+ - test/courgette/test_name_resolution.rb
150
+ - test/courgette/test_scope.rb
151
+ has_rdoc: