analyst 0.0.1 → 0.13.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.rspec +2 -0
  4. data/CODE_OF_CONDUCT.md +13 -0
  5. data/README.md +1 -0
  6. data/analyst.gemspec +12 -0
  7. data/lib/analyst/analyzer.rb +162 -0
  8. data/lib/analyst/cli.rb +42 -0
  9. data/lib/analyst/entity_parser/association.rb +24 -0
  10. data/lib/analyst/entity_parser/entities/class.rb +92 -0
  11. data/lib/analyst/entity_parser/entities/empty.rb +13 -0
  12. data/lib/analyst/entity_parser/entities/entity.rb +29 -0
  13. data/lib/analyst/entity_parser/entities/method.rb +16 -0
  14. data/lib/analyst/entity_parser/entities/module.rb +31 -0
  15. data/lib/analyst/formatters/base.rb +33 -0
  16. data/lib/analyst/formatters/csv.rb +43 -0
  17. data/lib/analyst/formatters/html.rb +87 -0
  18. data/lib/analyst/formatters/html_index.rb +47 -0
  19. data/lib/analyst/formatters/templates/index.html.haml +92 -0
  20. data/lib/analyst/formatters/templates/output.html.haml +114 -0
  21. data/lib/analyst/formatters/text.rb +56 -0
  22. data/lib/analyst/fukuzatsu/analyzer.rb +162 -0
  23. data/lib/analyst/fukuzatsu/cli.rb +42 -0
  24. data/lib/analyst/fukuzatsu/entity_parser/association.rb +24 -0
  25. data/lib/analyst/fukuzatsu/entity_parser/entities/class.rb +92 -0
  26. data/lib/analyst/fukuzatsu/entity_parser/entities/empty.rb +13 -0
  27. data/lib/analyst/fukuzatsu/entity_parser/entities/entity.rb +29 -0
  28. data/lib/analyst/fukuzatsu/entity_parser/entities/method.rb +16 -0
  29. data/lib/analyst/fukuzatsu/entity_parser/entities/module.rb +31 -0
  30. data/lib/analyst/fukuzatsu/formatters/base.rb +33 -0
  31. data/lib/analyst/fukuzatsu/formatters/csv.rb +43 -0
  32. data/lib/analyst/fukuzatsu/formatters/html.rb +87 -0
  33. data/lib/analyst/fukuzatsu/formatters/html_index.rb +47 -0
  34. data/lib/analyst/fukuzatsu/formatters/templates/index.html.haml +92 -0
  35. data/lib/analyst/fukuzatsu/formatters/templates/output.html.haml +114 -0
  36. data/lib/analyst/fukuzatsu/formatters/text.rb +56 -0
  37. data/lib/analyst/fukuzatsu/line_of_code.rb +19 -0
  38. data/lib/analyst/fukuzatsu/parsed_file.rb +85 -0
  39. data/lib/analyst/fukuzatsu/parsed_method.rb +32 -0
  40. data/lib/analyst/fukuzatsu/parser_original.rb +76 -0
  41. data/lib/analyst/fukuzatsu/rethink/parser.rb +346 -0
  42. data/lib/analyst/fukuzatsu/version.rb +3 -0
  43. data/lib/analyst/line_of_code.rb +19 -0
  44. data/lib/analyst/parsed_file.rb +85 -0
  45. data/lib/analyst/parsed_method.rb +32 -0
  46. data/lib/analyst/parser.rb +76 -0
  47. data/lib/analyst/rethink/parser.rb +346 -0
  48. data/lib/analyst/version.rb +1 -1
  49. data/lib/analyst.rb +17 -2
  50. data/spec/analyzer_spec.rb +122 -0
  51. data/spec/cli_spec.rb +48 -0
  52. data/spec/fixtures/eg_class.rb +8 -0
  53. data/spec/fixtures/eg_mod_class.rb +2 -0
  54. data/spec/fixtures/eg_mod_class_2.rb +5 -0
  55. data/spec/fixtures/eg_module.rb +2 -0
  56. data/spec/fixtures/module_with_class.rb +9 -0
  57. data/spec/fixtures/multiple_methods.rb +7 -0
  58. data/spec/fixtures/nested_methods.rb +8 -0
  59. data/spec/fixtures/program_1.rb +19 -0
  60. data/spec/fixtures/program_2.rb +25 -0
  61. data/spec/fixtures/program_3.rb +66 -0
  62. data/spec/fixtures/program_4.rb +1 -0
  63. data/spec/fixtures/single_class.rb +9 -0
  64. data/spec/fixtures/single_method.rb +3 -0
  65. data/spec/formatters/csv_spec.rb +37 -0
  66. data/spec/formatters/html_index_spec.rb +36 -0
  67. data/spec/formatters/html_spec.rb +48 -0
  68. data/spec/formatters/text_spec.rb +39 -0
  69. data/spec/parsed_file_spec.rb +67 -0
  70. data/spec/parsed_method_spec.rb +34 -0
  71. data/spec/spec_helper.rb +7 -0
  72. metadata +229 -2
@@ -0,0 +1,346 @@
1
+ require 'pry'
2
+
3
+ module Z
4
+
5
+ class Parser
6
+ extend Forwardable
7
+
8
+ attr_reader :corpus
9
+
10
+ def_delegators :@corpus, :classes, :associations
11
+
12
+ def initialize(path)
13
+ @corpus = Corpus.new(path)
14
+ end
15
+
16
+ def class_graph
17
+ {
18
+ nodes: corpus.classes,
19
+ edges: corpus.associations
20
+ #nodes: [1,2,3],
21
+ #edges: [{source: 1, target: 2, strength: 1}, {source: 2, target: 1, strength: 4}]
22
+ }
23
+ end
24
+ end
25
+
26
+
27
+ class Corpus
28
+
29
+ attr_reader :path, :classes, :associations
30
+
31
+ def initialize(path)
32
+ @path = path
33
+ parse!
34
+ end
35
+
36
+ def files
37
+ @files ||= file_paths.map {|path| File.new(path)}
38
+ end
39
+
40
+ def parse!
41
+ @classes = extract_classes
42
+ @associations = link_associations
43
+ #@associations = combine_identical_associations
44
+ end
45
+
46
+ private
47
+
48
+ def extract_classes
49
+ classes = []
50
+ files.each {|f| f.classes.each {|c| merge_class(classes, c) } }
51
+ classes
52
+ end
53
+
54
+ def link_associations
55
+ all = classes.map(&:associations).flatten
56
+ all.each do |assoc|
57
+ target = classes.detect {|c| c.full_name == assoc.target_class }
58
+ if target
59
+ assoc.target = target
60
+ else
61
+ puts "WARNING: Couldn't find target: #{assoc.target_class}"
62
+ end
63
+ end
64
+ end
65
+
66
+ def file_paths
67
+ if ::File.directory?(path)
68
+ Dir.glob(::File.join(path, "**", "*.rb"))
69
+ else
70
+ [path]
71
+ end
72
+ end
73
+
74
+ def merge_class(classes, klass)
75
+ existing = classes.find {|c| c.full_name == klass.full_name}
76
+ if existing
77
+ existing.merge(klass)
78
+ else
79
+ classes << klass
80
+ end
81
+ end
82
+
83
+ end
84
+
85
+
86
+ class File
87
+
88
+ attr_reader :path
89
+
90
+ def initialize(path)
91
+ @path = path
92
+ end
93
+
94
+ def contents
95
+ @contents ||= ::File.open(path, 'r').read
96
+ end
97
+
98
+ def ast
99
+ @ast ||= ::Parser::CurrentRuby.parse(contents)
100
+ end
101
+
102
+ def classes
103
+ return @classes if @classes
104
+ @classes = []
105
+ parse!
106
+ @classes
107
+ end
108
+
109
+ def parse!
110
+ parse_ast
111
+ true
112
+ end
113
+
114
+ private
115
+
116
+ # to log node types that haven't been dealt with
117
+ def unhandled_node_types
118
+ @unhandled_node_types ||= Set.new
119
+ end
120
+
121
+ # contains Module, Class, Method... eventually Block and other stuff maybe
122
+ def context_stack
123
+ @context_stack ||= [Entities::Empty.new]
124
+ end
125
+
126
+ def node_parsers
127
+ @node_parsers ||= Hash.new(:default_node_parser).merge!(
128
+ :class => :class_node_parser,
129
+ :module => :module_node_parser,
130
+ :def => :method_node_parser,
131
+ :send => :send_node_parser
132
+ # TODO: make a method parser, which pushes the the context_stack so that things inside method bodies
133
+ # are treated differently than those inside class or module bodies. same with Block (right?)
134
+ )
135
+ end
136
+
137
+ # search the whole tree for classes
138
+ def parse_ast(node=ast)
139
+ # files can start with (module), (class), or (begin)
140
+ # EDIT: or (send), or (def), or.... just about anything
141
+ return unless node.respond_to? :type
142
+ send(node_parsers[node.type], node)
143
+ end
144
+
145
+ def default_node_parser(node)
146
+ unhandled_node_types.add(node.type)
147
+ return unless node.respond_to? :children
148
+ node.children.each { |cnode| parse_ast(cnode) }
149
+ end
150
+
151
+ def class_node_parser(node)
152
+ name_node, super_node, content_node = node.children
153
+ klass = Entities::Class.new(context_stack.last, node)
154
+ @classes << klass
155
+ context_stack.push klass
156
+ parse_ast(content_node)
157
+ context_stack.pop
158
+ end
159
+
160
+ def module_node_parser(node)
161
+ name_node, content_node = node.children
162
+ mod = Entities::Module.new(context_stack.last, node)
163
+ context_stack.push mod
164
+ parse_ast(content_node)
165
+ context_stack.pop
166
+ end
167
+
168
+ def method_node_parser(node)
169
+ name, args_node, content_node = node.children
170
+ method = Entities::Method.new(context_stack.last, node)
171
+ context_stack.push method
172
+ parse_ast(content_node)
173
+ context_stack.pop
174
+ end
175
+
176
+ def send_node_parser(node)
177
+ target_node, method_name, *args = node.children
178
+ # if method_name is an association, then analyze the args to see what class it's associated with!
179
+ # and "it", btw, is the context_stack.last... so ya, also make sure this analysis is only done when we're in a
180
+ # class, NOT when we're in a method, NOT when we're in a defs, NOT when we're in an (sclass) (comes from
181
+ # class << self) -- got that?? those nodes should push something like 'Unsupported' onto the stack, or something
182
+ # just to keep track of where we are at any given point, and make sure we only care about sends that
183
+ # are truly at the right scope.
184
+ context_stack.last.handle_send_node(node)
185
+ # basically, if it's a class, it'll see if this is an association. if so, it'll store it by name. later on, we go
186
+ # thru and connect the pointers.
187
+ end
188
+ end
189
+
190
+
191
+ module Entities
192
+
193
+ class Entity
194
+ attr_reader :parent
195
+
196
+ def handle_send_node(node)
197
+ # abstract method. btw, this feels wrong -- send should be an entity too. but for now, whatevs.
198
+ end
199
+
200
+ def full_name
201
+ throw "this is abstract method, fool"
202
+ end
203
+
204
+ def inspect
205
+ "\#<#{self.class}:#{object_id} full_name=#{full_name}>"
206
+ end
207
+ end
208
+
209
+
210
+ class Empty < Entity
211
+ def full_name
212
+ ''
213
+ end
214
+ end
215
+
216
+
217
+ class Method < Entity
218
+ attr_reader :ast
219
+
220
+ def initialize(parent, ast)
221
+ @parent = parent
222
+ @ast = ast
223
+ end
224
+
225
+ def name
226
+ ast.children.first.to_s
227
+ end
228
+
229
+ def full_name
230
+ parent.full_name + '#' + name
231
+ end
232
+ end
233
+
234
+
235
+ class Module < Entity
236
+ attr_reader :ast
237
+
238
+ def initialize(parent, ast)
239
+ @parent = parent
240
+ @ast = ast
241
+ end
242
+
243
+ def name
244
+ const_node_array(ast.children.first).join('::')
245
+ end
246
+
247
+ def full_name
248
+ parent.full_name.empty? ? name : parent.full_name + '::' + name
249
+ end
250
+
251
+ private
252
+
253
+ # takes a (const) node and returns an array specifying the fully-qualified
254
+ # constant name that it represents. ya know, so CoolModule::SubMod::SweetClass
255
+ # would be parsed to:
256
+ # (const
257
+ # (const
258
+ # (const nil :CoolModule) :SubMod) :SweetClass)
259
+ # and passing that node here would return [:CoolModule, :SubMod, :SweetClass]
260
+ def const_node_array(node)
261
+ return [] if node.nil?
262
+ raise "expected (const) node or nil, got (#{node.type})" unless node.type == :const
263
+ const_node_array(node.children.first) << node.children[1]
264
+ end
265
+ end
266
+
267
+
268
+ class Class < Module
269
+
270
+ # pretend, for now, that we always have an AR. i.e., always look for associations.
271
+ ASSOCIATIONS = [:belongs_to, :has_one, :has_many, :has_and_belongs_to_many]
272
+
273
+ def handle_send_node(node)
274
+ # FIXME: this doesn't feel right, cuz (send) should probably be an entity too, especially
275
+ # since you can have nested sends... but i'm doing it this way for now.
276
+ target, method_name, *args = node.children
277
+ if ASSOCIATIONS.include? method_name
278
+ add_association(method_name, args)
279
+ end
280
+ end
281
+
282
+ def associations
283
+ @associations ||= []
284
+ end
285
+
286
+ def merge(other_class)
287
+ other_class.associations.each do |other_assoc|
288
+ duplicate = associations.detect do |assoc|
289
+ assoc.type == other_assoc.type && assoc.target_class == other_assoc.target_class
290
+ end
291
+ unless duplicate
292
+ associations << Association.new(type: other_assoc.type, source: self, target_class: other_assoc.target_class)
293
+ end
294
+ end
295
+ end
296
+
297
+ private
298
+
299
+ def add_association(method_name, args)
300
+ if args.size > 1
301
+ # args.last is a hash that might contain a class_name or a through
302
+ target_class = hash_val_str(args.last, :class_name)
303
+ end
304
+ target_class ||= begin
305
+ symbol_node = args.first
306
+ symbol_name = symbol_node.children.first
307
+ table_name = ::ActiveSupport::Inflector.pluralize(symbol_name)
308
+ ::ActiveSupport::Inflector.classify(table_name)
309
+ end
310
+ assoc = Association.new(type: method_name, source: self, target_class: target_class)
311
+ associations << assoc
312
+ end
313
+
314
+ # give it a (hash) node and a key (a symbol) and it'll look for that key in the hash and
315
+ # return the associated value as long as it's a string. if key isn't found, returns nil.
316
+ # if key is found but val isn't (str), throw exception. yep, this is pretty bespoke.
317
+ def hash_val_str(node, key)
318
+ return unless node.type == :hash
319
+ pair = node.children.detect do |pair_node|
320
+ key_sym_node = pair_node.children.first
321
+ key == key_sym_node.children.first
322
+ end
323
+ if pair
324
+ val_node = pair.children.last
325
+ throw "Bad type. Expected (str), got (#{val_node.type})" unless val_node.type == :str
326
+ val_node.children.first
327
+ end
328
+ end
329
+ end
330
+
331
+ end
332
+
333
+
334
+ class Association
335
+ attr_reader :type, :source, :target_class
336
+ attr_accessor :target
337
+
338
+ def initialize(type:, source:, target_class:)
339
+ @type = type
340
+ @source = source
341
+ @target_class = target_class
342
+ end
343
+ end
344
+
345
+ end
346
+
@@ -1,3 +1,3 @@
1
1
  module Analyst
2
- VERSION = "0.0.1"
2
+ VERSION = "0.13.1"
3
3
  end
data/lib/analyst.rb CHANGED
@@ -1,5 +1,20 @@
1
- require "analyst/version"
1
+ require 'ephemeral'
2
+ require 'poro_plus'
3
+ require 'fileutils'
4
+ require 'haml'
5
+
6
+ require_relative "analyst/analyzer"
7
+ require_relative "analyst/cli"
8
+ require_relative "analyst/formatters/base"
9
+ require_relative "analyst/formatters/csv"
10
+ require_relative "analyst/formatters/html"
11
+ require_relative "analyst/formatters/html_index"
12
+ require_relative "analyst/formatters/text"
13
+ require_relative "analyst/line_of_code"
14
+ require_relative "analyst/parsed_file"
15
+ require_relative "analyst/parsed_method"
16
+ require_relative "analyst/parser"
17
+ require_relative "analyst/version"
2
18
 
3
19
  module Analyst
4
- # Your code goes here...
5
20
  end
@@ -0,0 +1,122 @@
1
+ require 'spec_helper'
2
+
3
+ describe Analyzer do
4
+
5
+ let(:content_1) { File.read("spec/fixtures/program_1.rb") }
6
+ let(:content_2) { File.read("spec/fixtures/program_2.rb") }
7
+ let(:content_3) { File.read("spec/fixtures/eg_class.rb") }
8
+ let(:content_4) { File.read("spec/fixtures/eg_mod_class.rb") }
9
+ let(:content_5) { File.read("spec/fixtures/eg_module.rb") }
10
+ let(:content_6) { File.read("spec/fixtures/eg_mod_class_2.rb") }
11
+
12
+ context "#extract_class_name" do
13
+
14
+ context "from Class Foo" do
15
+ it "returns Foo" do
16
+ expect(Analyzer.new(content_3).extract_class_name).to eq("Foo")
17
+ end
18
+ end
19
+
20
+ context "from Module::Class Foo" do
21
+ it "returns Foo::Bar" do
22
+ expect(Analyzer.new(content_4).extract_class_name).to eq("Foo::Bar")
23
+ end
24
+ end
25
+
26
+ context "from Module; Class" do
27
+ it "returns Extracted::Thing" do
28
+ expect(Analyzer.new(content_6).extract_class_name).to eq("Extracted::Thing")
29
+ end
30
+ end
31
+
32
+ context "from Module" do
33
+ it "returns Something" do
34
+ expect(Analyzer.new(content_5).extract_class_name).to eq("Something")
35
+ end
36
+ end
37
+ end
38
+
39
+ describe "#complexity" do
40
+ context "program_1" do
41
+
42
+ let(:analyzer) { Analyzer.new(content_1) }
43
+
44
+ it "matches the manual calculation of complexity of 4" do
45
+ expect(analyzer.complexity).to eq(4)
46
+ end
47
+
48
+ end
49
+
50
+ context "program_2" do
51
+
52
+ let(:analyzer) { Analyzer.new(content_2) }
53
+
54
+ it "matches the manual calculation of complexity of 5" do
55
+ expect(analyzer.complexity).to eq(5)
56
+ end
57
+
58
+ end
59
+ end
60
+
61
+ describe "extract_class_name" do
62
+ context "from a file with a class in it" do
63
+ let(:analyzer) { Analyzer.new(File.read("spec/fixtures/single_class.rb")) }
64
+ it "should return the name of the class" do
65
+ expect(analyzer.extract_class_name).to eq "Gown"
66
+ end
67
+ end
68
+ context "from a file with a class inside a module" do
69
+ let(:analyzer) { Analyzer.new(File.read("spec/fixtures/module_with_class.rb")) }
70
+ it "should return the name of the class" do
71
+ expect(analyzer.extract_class_name).to eq "Symbolics::Insects::Bee"
72
+ end
73
+ end
74
+ context "from a file with no class in it" do
75
+ let(:analyzer) { Analyzer.new(File.read("spec/fixtures/single_method.rb")) }
76
+ it "should return 'Unknown'" do
77
+ expect(analyzer.extract_class_name).to eq "Unknown"
78
+ end
79
+ end
80
+
81
+ end
82
+
83
+ describe "extract_methods" do
84
+ # Note: should implicitly trust private method #methods_from
85
+ context "from a file with a single method" do
86
+ let(:analyzer) { Analyzer.new(File.read("spec/fixtures/single_method.rb")) }
87
+ it "should return a single method" do
88
+ expect(analyzer.extract_methods.count).to eq 1
89
+ end
90
+ it "should extract the method name" do
91
+ expect(analyzer.extract_methods[0].name).to eq "#read_poem"
92
+ end
93
+ it "should extract the method content" do
94
+ expect(analyzer.extract_methods[0].content).to eq 'def read_poem
95
+ return "I meant to find her when I came/Death had the same design"
96
+ end'
97
+ end
98
+ it "should set type to :instance" do
99
+ expect(analyzer.extract_methods[0].type).to eq :instance
100
+ end
101
+ end
102
+ context "from a file with multiple methods" do
103
+ let(:analyzer) { Analyzer.new(File.read("spec/fixtures/multiple_methods.rb")) }
104
+ it "should return multiple methods" do
105
+ expect(analyzer.extract_methods.map { |m| m.name }).to eq ["#bake_treats", "#lower_from_window"]
106
+ end
107
+ end
108
+ context "from a file with nested methods" do
109
+ let(:analyzer) { Analyzer.new(File.read("spec/fixtures/nested_methods.rb")) }
110
+ it "should return the root method, and its child" do
111
+ expect(analyzer.extract_methods.map { |m| m.name }).to eq ["#grow_flowers", "#water_earth"]
112
+ end
113
+ end
114
+ context "from a file with a class" do
115
+ let(:analyzer) { Analyzer.new(File.read("spec/fixtures/single_class.rb")) }
116
+ it "should return the class and its methods" do
117
+ expect(analyzer.extract_methods.map { |m| m.name }).to eq ["#initialize", "#color"]
118
+ end
119
+ end
120
+ end
121
+
122
+ end
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Analyst::CLI" do
4
+
5
+ let(:cli) { Analyst::CLI.new([
6
+ path: "foo/bar.rb",
7
+ format: 'text'
8
+ ]
9
+ )}
10
+
11
+ let(:summary_1) {
12
+ {
13
+ results_file: "doc/analyst/file_1.rb.htm",
14
+ path_to_file: "file_1.rb",
15
+ class_name: "File_1",
16
+ complexity: 13
17
+ }
18
+ }
19
+
20
+ let(:summary_2) {
21
+ {
22
+ results_file: "doc/analyst/file_2.rb.htm",
23
+ path_to_file: "file_2.rb",
24
+ class_name: "File_2",
25
+ complexity: 11
26
+ }
27
+ }
28
+
29
+ before do
30
+ allow(cli).to receive(:summaries) { [summary_1, summary_2] }
31
+ end
32
+
33
+ describe "#formatter" do
34
+ it "returns a csv formatter" do
35
+ allow(cli).to receive(:options) { {'format' => 'csv'} }
36
+ expect(cli.send(:formatter)).to eq Formatters::Csv
37
+ end
38
+ it "returns an html formatter" do
39
+ allow(cli).to receive(:options) { {'format' => 'html'} }
40
+ expect(cli.send(:formatter)).to eq Formatters::Html
41
+ end
42
+ it "returns a text formatter" do
43
+ allow(cli).to receive(:options) { {'format' => 'text'} }
44
+ expect(cli.send(:formatter)).to eq Formatters::Text
45
+ end
46
+ end
47
+
48
+ end
@@ -0,0 +1,8 @@
1
+ require 'analyst'
2
+
3
+ class Foo
4
+ def say_hello
5
+ "Oh hi there."
6
+ end
7
+ end
8
+
@@ -0,0 +1,2 @@
1
+ class Foo::Bar
2
+ end
@@ -0,0 +1,5 @@
1
+ module Extracted
2
+ class Thing
3
+ end
4
+ end
5
+
@@ -0,0 +1,2 @@
1
+ module Something
2
+ end
@@ -0,0 +1,9 @@
1
+ module Symbolics
2
+ module Insects
3
+ class Bee
4
+ def correspond(letter)
5
+ @letter = letter
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ def bake_treats
2
+ return :a_basket_of_cookies
3
+ end
4
+
5
+ def lower_from_window(window)
6
+ window.insert(bake_treats)
7
+ end
@@ -0,0 +1,8 @@
1
+ def grow_flowers(seed, water, earth)
2
+ def water_earth(water, earth)
3
+ earth.insert(water)
4
+ end
5
+
6
+ earth.insert.seed
7
+ 50.times(water_earth(water, earth))
8
+ end
@@ -0,0 +1,19 @@
1
+ def spaghetti
2
+ if 1 < 5
3
+ p 'hi'
4
+ else
5
+ if 4 ==5
6
+ if 2 > 0
7
+ p 'bye'
8
+ else
9
+ p 'see ya'
10
+ end
11
+ else
12
+ if 2 > 0
13
+ p 'bye'
14
+ else
15
+ p 'see ya'
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ def toppings
2
+
3
+ if rand(10) < 5
4
+ return "meatball"
5
+ end
6
+
7
+ if 1 < 5
8
+ return 'tomato'
9
+ else
10
+ if 4 ==5
11
+ if 2 > 0
12
+ return "parmesan"
13
+ else
14
+ return "romano"
15
+ end
16
+ else
17
+ if 2 > 0
18
+ return "alfredo"
19
+ else
20
+ return "gravy"
21
+ end
22
+ end
23
+ end
24
+
25
+ end