explain 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use rbx-head@explain --create
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in explain.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Josep M. Bach
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,69 @@
1
+ # explain
2
+
3
+ Explain explains your Ruby code in natural language. It is intended to be a
4
+ tool for beginners who aren't yet very familiar with programming.
5
+
6
+ It is a work in progress (a bit rough on the edges), so don't be mad. It will
7
+ get better over time ;)
8
+
9
+ (Explain runs only on Rubinius.)
10
+
11
+ ## Installation
12
+
13
+ Install Rubinius if you don't have it yet:
14
+
15
+ $ rvm install rbx-head
16
+ $ rvm use rbx-head
17
+
18
+ Install explain as a gem:
19
+
20
+ $ gem install explain
21
+
22
+ ## Usage
23
+
24
+ Given some example Ruby code, for example this in `examples/person.rb`:
25
+
26
+ ```ruby
27
+ class Person
28
+ def walk(distance)
29
+ @distance += distance
30
+ @hunger += 2
31
+ end
32
+
33
+ def eat(food)
34
+ @hunger -= food.nutritional_value
35
+ end
36
+ end
37
+ ```
38
+
39
+ We execute `explain` from the command line:
40
+
41
+ $ explain examples/person.rb
42
+
43
+ And it will output:
44
+
45
+ Let's describe the general attributes and behavior of any Person.
46
+
47
+ A Person can **walk**, given a specific distance. This is described as
48
+ follows: its distance will be its distance plus what we previously defined as
49
+ `distance`. Finally we return its hunger will be its hunger plus the number
50
+ 2..
51
+
52
+ A Person can **eat**, given a specific food. This is described as follows:
53
+ Finally we return its hunger will be its hunger minus the result of calling
54
+ **nutritional_value** on what we previously defined as `food`..
55
+ And with this we're done describing a Person.
56
+
57
+ ## Contributing
58
+
59
+ 1. Fork it
60
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
61
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
62
+ 4. Push to the branch (`git push origin my-new-feature`)
63
+ 5. Create new Pull Request
64
+
65
+ ## Who's this
66
+
67
+ This was made by [Josep M. Bach (Txus)](http://txustice.me) under the MIT
68
+ license. I'm [@txustice](http://twitter.com/txustice) on twitter (where you
69
+ should probably follow me!).
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/**/*_test.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env rbx
2
+
3
+ $: << 'lib'
4
+ require 'explain'
5
+ puts Explain.explain File.read(ARGV.first)
@@ -0,0 +1,10 @@
1
+ class Person
2
+ def walk(distance)
3
+ @distance += distance
4
+ @hunger += 2
5
+ end
6
+
7
+ def eat(food)
8
+ @hunger -= food.nutritional_value
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'explain/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "explain"
8
+ gem.version = Explain::VERSION
9
+ gem.authors = ["Josep M. Bach"]
10
+ gem.email = ["josep.m.bach@gmail.com"]
11
+ gem.description = %q{Explain explains your Ruby code in natural language.}
12
+ gem.summary = %q{Explain explains your Ruby code in natural language.}
13
+ gem.homepage = "https://github.com/txus/explain"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
@@ -0,0 +1,10 @@
1
+ require "explain/version"
2
+ require "explain/visitor"
3
+
4
+ module Explain
5
+ def self.explain(code)
6
+ visitor = Visitor.new
7
+ visitor.visit(code.to_ast)
8
+ visitor.output
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module Explain
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,337 @@
1
+ module Explain
2
+ class Visitor
3
+ def initialize
4
+ @output = []
5
+ end
6
+
7
+ def visit(node, in_sentence=false)
8
+ __send__ node.node_name, node, in_sentence
9
+ end
10
+
11
+ def emit(str)
12
+ @output.push str
13
+ end
14
+
15
+ def output
16
+ @output.join
17
+ end
18
+
19
+ def class(node, in_sentence=false)
20
+ @in_class = node.name.name
21
+ emit "Let's describe the general attributes and behavior of any #{node.name.name}."
22
+ visit node.body
23
+ emit "\nAnd with this we're done describing a #{node.name.name}."
24
+ @in_class = nil
25
+ end
26
+
27
+ def module(node, in_sentence=false)
28
+ @in_class = node.name.name
29
+ emit "Let's describe a collection of behavior that we'll call #{node.name.name}."
30
+ visit node.body
31
+ emit "\nAnd with this we're done describing #{node.name.name}."
32
+ @in_class = nil
33
+ end
34
+
35
+ def empty_body(*)
36
+ # do nothing
37
+ end
38
+
39
+ def class_scope(node, in_sentence=false)
40
+ visit node.body
41
+ end
42
+ alias module_scope class_scope
43
+
44
+ def local_variable_assignment(node, in_sentence=false)
45
+ emit in_sentence ? "w" : "W"
46
+ emit "e define as `#{node.name}` "
47
+ visit(node.value)
48
+ end
49
+
50
+ def local_variable_access(node, in_sentence=false)
51
+ emit "what we previously defined as `#{node.name}`"
52
+ end
53
+
54
+ def instance_variable_assignment(node, in_sentence=false)
55
+ emit in_sentence ? "i" : "I"
56
+ emit "ts #{node.name[1..-1]} will be "
57
+ visit(node.value)
58
+ end
59
+
60
+ def instance_variable_access(node, in_sentence=false)
61
+ emit "its #{node.name[1..-1]}"
62
+ end
63
+
64
+ def fixnum_literal(node, in_sentence=false)
65
+ emit "the number #{node.value}"
66
+ end
67
+
68
+ def float_literal(node, in_sentence=false)
69
+ emit "the number #{node.value}"
70
+ end
71
+
72
+ def string_literal(node, in_sentence=false)
73
+ emit "the word \"#{node.string}\""
74
+ end
75
+
76
+ def symbol_literal(node, in_sentence=false)
77
+ emit "the name `#{node.value}`"
78
+ end
79
+
80
+ def true_literal(node, in_sentence=false)
81
+ emit "the truth"
82
+ end
83
+
84
+ def false_literal(node, in_sentence=false)
85
+ emit "the falsehood"
86
+ end
87
+
88
+ def nil_literal(node, in_sentence=false)
89
+ emit "the nothingness"
90
+ end
91
+
92
+ def empty_array(node, in_sentence=false)
93
+ emit "an empty list"
94
+ end
95
+
96
+ def array_literal(node, in_sentence=false)
97
+ body = node.body
98
+ emit "a list of "
99
+ body.each_with_index do |node, index|
100
+ visit node
101
+ if body.length == index + 2 # last element
102
+ emit " and "
103
+ elsif body.length != index + 1
104
+ emit ", "
105
+ end
106
+ end
107
+ end
108
+
109
+ def hash_literal(node, in_sentence=false)
110
+ return emit "an empty dictionary" if node.array.empty?
111
+
112
+ body = node.array.each_slice(2)
113
+
114
+ emit 'a dictionary where '
115
+ body.each_with_index do |slice, index|
116
+ key, value = slice
117
+
118
+ visit key
119
+ emit " relates to "
120
+ visit value
121
+
122
+ if body.to_a.length == index + 2 # last element
123
+ emit " and "
124
+ elsif body.to_a.length != index + 1
125
+ emit ", "
126
+ end
127
+ end
128
+ end
129
+
130
+ # def range(node, in_sentence=false)
131
+ # end
132
+
133
+ # def range_exclude(node, in_sentence=false)
134
+ # end
135
+
136
+ # def regex_literal(node, in_sentence=false)
137
+ # end
138
+
139
+ def send(node, in_sentence=false)
140
+ if node.receiver.is_a?(Rubinius::AST::Self)
141
+ emit in_sentence ? "the result of calling " : "We "
142
+ emit "**#{node.name}**"
143
+ else
144
+ emit in_sentence ? "the result of calling " : "We call "
145
+ emit "**#{node.name}** on "
146
+ visit node.receiver
147
+ end
148
+ end
149
+
150
+ def send_with_arguments(node, in_sentence=false)
151
+ return if process_binary_operator(node, in_sentence) # 1 * 2, a / 3, true && false
152
+
153
+ if node.receiver.is_a?(Rubinius::AST::Self)
154
+ emit in_sentence ? "the result of calling " : "We "
155
+ emit "**#{node.name}**"
156
+ else
157
+ emit in_sentence ? "the result of calling " : "We call "
158
+ emit "**#{node.name}** on "
159
+ visit node.receiver
160
+ end
161
+
162
+ unless node.arguments.array.empty?
163
+ emit " given "
164
+ visit(node.arguments)
165
+ end
166
+ end
167
+
168
+ def actual_arguments(node, in_sentence=false)
169
+ body = node.array
170
+ body.each_with_index do |node, index|
171
+ visit node
172
+ if body.length == index + 2 # last element
173
+ emit " and "
174
+ elsif body.length != index + 1
175
+ emit ", "
176
+ end
177
+ end
178
+ end
179
+
180
+ # def iter_arguments(node, in_sentence=false)
181
+ # end
182
+
183
+ # def iter(node, in_sentence=false)
184
+ # end
185
+
186
+ def block(node, in_sentence=false)
187
+ body = node.array
188
+ body.each_with_index do |node, index|
189
+ if body.length == index + 1 # last element
190
+ if @in_method
191
+ emit "Finally we return "
192
+ end
193
+ visit node, true
194
+ emit "."
195
+ else
196
+ visit node, in_sentence
197
+ emit '. '
198
+ end
199
+ end
200
+ end
201
+
202
+ # def not(node, in_sentence=false)
203
+ # end
204
+
205
+ # def and(node, in_sentence=false)
206
+ # end
207
+
208
+ # def or(node, in_sentence=false)
209
+ # end
210
+
211
+ # def op_assign_and(node, in_sentence=false)
212
+ # end
213
+
214
+ # def op_assign_or(node, in_sentence=false)
215
+ # end
216
+
217
+ # def toplevel_constant(node, in_sentence=false)
218
+ # end
219
+
220
+ # def constant_access(node, in_sentence=false)
221
+ # end
222
+
223
+ # def scoped_constant(node, in_sentence=false)
224
+ # end
225
+
226
+ def if(node, in_sentence=false)
227
+ body, else_body = node.body, node.else
228
+ keyword = 'if'
229
+
230
+ if node.body.is_a?(Rubinius::AST::NilLiteral) && !node.else.is_a?(Rubinius::AST::NilLiteral)
231
+
232
+ body, else_body = else_body, body
233
+ keyword = 'unless'
234
+ end
235
+
236
+ emit "#{keyword} "
237
+ visit node.condition, true
238
+ emit " is truthy, then "
239
+
240
+ visit body, true
241
+
242
+ if else_body.is_a?(Rubinius::AST::NilLiteral)
243
+ return
244
+ end
245
+
246
+ emit " -- else, "
247
+
248
+ visit else_body, true
249
+ end
250
+
251
+ def while(node, in_sentence=false)
252
+ emit in_sentence ? "w" : "W"
253
+ emit 'hile '
254
+ visit node.condition, true
255
+ emit ", "
256
+ visit node.body, true
257
+ end
258
+
259
+ def until(node, in_sentence=false)
260
+ emit in_sentence ? "u" : "U"
261
+ emit 'ntil '
262
+ visit node.condition, true
263
+ emit ", "
264
+ visit node.body, true
265
+ end
266
+
267
+ def return(node, in_sentence=false)
268
+ emit in_sentence ? "w" : "W"
269
+ emit "e return "
270
+ visit node.value
271
+ end
272
+
273
+ def define(node, in_sentence=false)
274
+ args = enumerate(node.arguments.names)
275
+
276
+ if @in_class
277
+ emit "\n\nA #{@in_class} can **#{node.name}**"
278
+ else
279
+ emit "\n\nLet's define a method: **#{node.name}**"
280
+ end
281
+ if node.arguments.names.empty?
282
+ emit "."
283
+ else
284
+ emit ", given a specific #{args}."
285
+ end
286
+
287
+ unless node.body.array.first.is_a?(Rubinius::AST::NilLiteral) && node.body.array.length == 1
288
+ @in_method = true
289
+ emit " This is described as follows: "
290
+ visit node.body, true
291
+ @in_method = false
292
+ end
293
+ end
294
+
295
+ def method_missing(m, *args, &block)
296
+ emit "(eerr I don't know how to explain `#{m}`)"
297
+ end
298
+
299
+ private
300
+
301
+ def enumerate(ary)
302
+ comma_joinable = ary[0..-2]
303
+ if comma_joinable.empty?
304
+ "#{ary[-1]}"
305
+ else
306
+ [comma_joined.join(', '), ary[-1]].join(' and ')
307
+ end
308
+ end
309
+
310
+ def process_binary_operator(node, in_sentence)
311
+ operators = %w(+ - * / & | << < > >= <= == !=).map(&:to_sym)
312
+ return false unless operators.include?(node.name)
313
+ return false if node.arguments.array.length != 1
314
+
315
+ operand = node.arguments.array[0]
316
+
317
+ unless node.receiver.is_a?(Rubinius::AST::Self)
318
+ visit node.receiver
319
+ end
320
+
321
+ emit case node.name.to_s
322
+ when "+" then " plus "
323
+ when "-" then " minus "
324
+ when "*" then " times "
325
+ when "/" then " divided by "
326
+ when "<" then " is less than "
327
+ when ">" then " is greater than "
328
+ when ">=" then " is greater or equal than "
329
+ when "<=" then " is less or equal than "
330
+ when "==" then " is equal to "
331
+ when "!=" then " is different than "
332
+ end
333
+
334
+ visit operand, true
335
+ end
336
+ end
337
+ end
@@ -0,0 +1,90 @@
1
+ require 'test_helper'
2
+ require 'explain/visitor'
3
+
4
+ module Explain
5
+ describe Visitor do
6
+ let(:code) { "a = 123".to_ast }
7
+ let(:visitor) { Visitor.new }
8
+
9
+ def self.compiles(code, expected)
10
+ it "explains #{code}" do
11
+ visitor.visit(code.to_ast)
12
+ visitor.output.must_equal(expected)
13
+ end
14
+ end
15
+
16
+ # Basic types
17
+ compiles %q{123},
18
+ %q{the number 123}
19
+ compiles %q{12.3},
20
+ %q{the number 12.3}
21
+ compiles %q{:hey},
22
+ %q{the name `hey`}
23
+ compiles %q{"hey"},
24
+ %q{the word "hey"}
25
+
26
+ compiles %q{true},
27
+ %q{the truth}
28
+ compiles %q{false},
29
+ %q{the falsehood}
30
+ compiles %q{nil},
31
+ %q{the nothingness}
32
+
33
+ compiles %q{[]},
34
+ %q{an empty list}
35
+ compiles %q{["hey", 9.9, 123]},
36
+ %q{a list of the word "hey", the number 9.9 and the number 123}
37
+ compiles %q{{}},
38
+ %q{an empty dictionary}
39
+ compiles %q{{:foo => "bar", :baz => 123}},
40
+ %q{a dictionary where the name `foo` relates to the word "bar" and the name `baz` relates to the number 123}
41
+
42
+ # Assignments
43
+ compiles %q{a = 123},
44
+ %q{We define as `a` the number 123}
45
+ compiles %q{name = "John"},
46
+ %q{We define as `name` the word "John"}
47
+
48
+ compiles %q{def ret_name; name = "John"; name; end},
49
+ %Q{\n\nLet's define a method: **ret_name**. This is described as follows: we define as `name` the word "John". Finally we return what we previously defined as `name`.}
50
+ compiles %q{@name},
51
+ %q{its name}
52
+ compiles %q{@name = "John"},
53
+ %q{Its name will be the word "John"}
54
+
55
+ # Classes and modules
56
+ compiles %q{class Person; end},
57
+ %Q{Let's describe the general attributes and behavior of any Person.\nAnd with this we're done describing a Person.}
58
+
59
+ compiles %q{class Person
60
+ def walk(distance)
61
+ @position += distance
62
+ update_hunger 1
63
+ end
64
+ end},
65
+ %Q{Let's describe the general attributes and behavior of any Person.\n\nA Person can **walk**, given a specific distance. This is described as follows: its position will be its position plus what we previously defined as `distance`. Finally we return the result of calling **update_hunger** given the number 1..\nAnd with this we're done describing a Person.}
66
+
67
+ # Control flow
68
+ compiles %q{if true
69
+ false
70
+ else
71
+ :foo
72
+ end},
73
+ %Q{if the truth is truthy, then the falsehood -- else, the name `foo`}
74
+
75
+ compiles %q{number = 0
76
+ while number < 3
77
+ number += 1
78
+ end},
79
+ %Q{We define as `number` the number 0. while what we previously defined as `number` is less than the number 3, we define as `number` what we previously defined as `number` plus the number 1.}
80
+
81
+ compiles %q{number = 0
82
+ until number == 3
83
+ number += 1
84
+ end},
85
+ %Q{We define as `number` the number 0. until what we previously defined as `number` is equal to the number 3, we define as `number` what we previously defined as `number` plus the number 1.}
86
+
87
+ compiles %q{return 3},
88
+ %Q{We return the number 3}
89
+ end
90
+ end
@@ -0,0 +1,2 @@
1
+ require 'minitest/spec'
2
+ require 'minitest/autorun'
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: explain
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Josep M. Bach
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-07 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Explain explains your Ruby code in natural language.
15
+ email:
16
+ - josep.m.bach@gmail.com
17
+ executables:
18
+ - explain
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - .gitignore
23
+ - .rvmrc
24
+ - Gemfile
25
+ - LICENSE.txt
26
+ - README.md
27
+ - Rakefile
28
+ - bin/explain
29
+ - examples/person.rb
30
+ - explain.gemspec
31
+ - lib/explain.rb
32
+ - lib/explain/version.rb
33
+ - lib/explain/visitor.rb
34
+ - test/explain/visitor_test.rb
35
+ - test/test_helper.rb
36
+ homepage: https://github.com/txus/explain
37
+ licenses: []
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ none: false
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ none: false
54
+ requirements: []
55
+ rubyforge_project:
56
+ rubygems_version: 1.8.24
57
+ signing_key:
58
+ specification_version: 3
59
+ summary: Explain explains your Ruby code in natural language.
60
+ test_files:
61
+ - test/explain/visitor_test.rb
62
+ - test/test_helper.rb