analyst 0.0.1 → 0.13.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.
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