ffast 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7d9179d0dbc56cc0f493cdbff51cd926b4476c37
4
- data.tar.gz: 953cef108b8207d8d83aa16681f8f31e3c79b8f8
3
+ metadata.gz: 238c006c217b8838c23488cbfafe15c3e694d69a
4
+ data.tar.gz: '095e4dfdba32adf0ddf52da166c27a93c8bbd3bb'
5
5
  SHA512:
6
- metadata.gz: 25b1bb7d048b2784a8127b60841ed8946495bacace7ad0caa8acc6f256aa1c2b3116491898d8b276321a0c5db847d350763c1bc18ef88c2cfaee4b6fa7a8c682
7
- data.tar.gz: e8e7e051223d4b1c2bb77d5d44c742518f38c7d9bf75c49342dfa495c4c5252e72ed6890d9c6d68c6faf531e1e1166391ae0cd79f20162ed81df77036bd14413
6
+ metadata.gz: 73ce257e59e1bd96cbc62d4b23225cd50f5bfa551ca64eeb38da4693bc832b14203d2335d6e55e389b74fd5feba1e164ed2f1adbcdbeca0a14676115e1df5c02
7
+ data.tar.gz: 3133c3f337da981e728ab42ed9bf61eafc907715c3c6bb7c210ff38d6a6c73d3e87cf88625d8cac4dcfa648d3f909b3e6625a502d96c9a2316e04436086c7f8f
data/.rubocop.yml ADDED
@@ -0,0 +1,37 @@
1
+ # This is the configuration used to check the rubocop source code.
2
+
3
+ require:
4
+ - rubocop-rspec
5
+
6
+ AllCops:
7
+ Exclude:
8
+ - 'tmp/**/*'
9
+ - 'examples/*'
10
+ TargetRubyVersion: 2.3
11
+
12
+ Metrics/LineLength:
13
+ Enabled: false
14
+
15
+ Metrics/BlockLength:
16
+ Exclude:
17
+ - 'spec/**/*'
18
+
19
+ Lint/InterpolationCheck:
20
+ Exclude:
21
+ - 'spec/**/*'
22
+
23
+ Metrics/MethodLength:
24
+ CountComments: false # count full line comments?
25
+ Max: 12
26
+
27
+ Metrics/ModuleLength:
28
+ Enabled: false
29
+
30
+ Layout/MultilineMethodCallIndentation:
31
+ EnforcedStyle: 'indented'
32
+
33
+ RSpec/NestedGroups:
34
+ Max: 4
35
+
36
+ RSpec/ExampleLength:
37
+ Max: 20
data/.travis.yml CHANGED
@@ -3,3 +3,12 @@ language: ruby
3
3
  rvm:
4
4
  - 2.3.3
5
5
  before_install: gem install bundler -v 1.13.7
6
+ before_script:
7
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
8
+ - chmod +x ./cc-test-reporter
9
+ - ./cc-test-reporter before-build
10
+ after_script:
11
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
12
+ script:
13
+ - bundle exec rubocop
14
+ - bundle exec rspec
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in fast.gemspec
data/Guardfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # A sample Guardfile
2
4
  # More info at https://github.com/guard/guard#readme
3
5
 
@@ -19,8 +21,8 @@ guard 'livereload' do
19
21
  watch(%r{lib/.+\.rb$})
20
22
  end
21
23
 
22
- guard :rspec, cmd: "bundle exec rspec" do
23
- require "guard/rspec/dsl"
24
+ guard :rspec, cmd: 'bundle exec rspec' do
25
+ require 'guard/rspec/dsl'
24
26
  dsl = Guard::RSpec::Dsl.new(self)
25
27
 
26
28
  # Feel free to open issues for suggestions and improvements
data/README.md CHANGED
@@ -4,21 +4,33 @@
4
4
 
5
5
  Fast is a "Find AST" tool to help you search in the code abstract syntax tree.
6
6
 
7
- It's inspired on [RuboCop Node Pattern](https://github.com/bbatsov/rubocop/blob/master/lib/rubocop/node_pattern.rb).
7
+ Ruby allow us to do the same thing in a few ways then it's hard to check
8
+ how the code is written.
8
9
 
9
- To learn more about how AST works, you can install `ruby-parse` and check how is the AST of
10
- your current code.
10
+ ## Syntax for find in AST
11
11
 
12
- `ruby-parse my-file.rb`
12
+ The current version cover the following elements:
13
13
 
14
- It will output the AST representation.
14
+ - `()` to represent a **node** search
15
+ - `{}` is for **any** matches like **union** conditions with **or** operator
16
+ - `[]` is for **all** matches like **intersect** conditions with **and** operator
17
+ - `$` is for **capture** current expression
18
+ - `_` is **something** not nil
19
+ - `nil` matches exactly **nil**
20
+ - `...` is a **node** with children
21
+ - `^` is to get the **parent node** of an expression
22
+ - `?` is for **maybe**
23
+ - `\1` to use the first **previous captured** element
24
+ - `""` surround the value with double quotes to match literal strings
25
+
26
+ The syntax is inspired on [RuboCop Node Pattern](https://github.com/bbatsov/rubocop/blob/master/lib/rubocop/node_pattern.rb).
15
27
 
16
28
  ## Installation
17
29
 
18
30
  Add this line to your application's Gemfile:
19
31
 
20
32
  ```ruby
21
- gem 'fast'
33
+ gem 'ffast'
22
34
  ```
23
35
 
24
36
  And then execute:
@@ -27,44 +39,119 @@ And then execute:
27
39
 
28
40
  Or install it yourself as:
29
41
 
30
- $ gem install fast
42
+ $ gem install ffast
31
43
 
32
44
  ## How it works
33
45
 
34
46
  The idea is search in abstract tree using a simple expression build with an array:
35
47
 
36
- The following code:
48
+ A simple integer in ruby:
37
49
 
38
50
  ```ruby
39
- a += 1
51
+ 1
40
52
  ```
41
53
 
42
- Generates the following AST representation:
54
+ Is represented by:
43
55
 
44
56
  ```ruby
45
- ast =
46
- s(:op_asgn,
47
- s(:lvasgn, :a),
48
- :+,
49
- s(:int, 1)
50
- )
57
+ s(:int, 1)
51
58
  ```
52
59
 
53
60
  Basically `s` represents `Parser::AST::Node` and the node has a `#type` and `#children`.
54
61
 
55
- You can try to search by nodes that is using `:op_asgn` with some children using `...`:
62
+ ```ruby
63
+ def s(type, *children)
64
+ Parser::AST::Node.new(type, children)
65
+ end
66
+ ```
67
+
68
+ A local variable assignment:
69
+
70
+ ```ruby
71
+ value = 42
72
+ ```
73
+
74
+ Can be represented with:
75
+
76
+ ```ruby
77
+ ast = s(:lvasgn, :value, s(:int, 42))
78
+ ```
79
+
80
+ Now, lets find local variable named `value` with an value `42`:
81
+
82
+ ```ruby
83
+ Fast.match?(ast, '(lvasgn value (int 42))') # true
84
+ ```
85
+
86
+ Lets abstract a bit and allow some integer value using `_` as a shortcut:
87
+
88
+ ```ruby
89
+ Fast.match?(ast, '(lvasgn value (int _))') # true
90
+ ```
91
+
92
+ Lets abstract more and allow float or integer:
93
+
94
+ ```ruby
95
+ Fast.match?(ast, '(lvasgn value ({float int} _))') # true
96
+ ```
97
+
98
+ Or combine multiple assertions using `[]` to join conditions:
99
+
100
+ ```ruby
101
+ Fast.match?(ast, '(lvasgn value ([!str !hash !array] _))') # true
102
+ ```
103
+
104
+ Matches all local variables not string **and** not hash **and** not array.
105
+
106
+ We can match "a node with children" using `...`:
107
+
108
+ ```ruby
109
+ Fast.match?(ast, '(lvasgn value ...)') # true
110
+ ```
111
+
112
+ You can use `$` to capture a node:
56
113
 
57
114
  ```ruby
58
- Fast.match?(ast, [:op_asgn, '...']) # => true
115
+ Fast.match?(ast, '(lvasgn value $...)') # => [s(:int, 42)]
59
116
  ```
60
117
 
61
- You can also check if the element is not nil with `_`:
118
+ Or match whatever local variable assignment combining both `_` and `...`:
62
119
 
63
120
  ```ruby
64
- Fast.match?(ast, [:op_asgn, '_', '_', '_'])) # => true
121
+ Fast.match?(ast, '(lvasgn _ ...)') # true
65
122
  ```
66
123
 
67
- You can go deeply with the arrays. Let's suppose we have a hardcore call to
124
+ You can also use captures in any levels you want:
125
+
126
+ ```ruby
127
+ Fast.match?(ast, '(lvasgn $_ $...)') # [:value, s(:int, 42)]
128
+ ```
129
+
130
+ Keep in mind that `_` means something not nil and `...` means a node with
131
+ children.
132
+
133
+ Then, if do you get a method declared:
134
+
135
+ ```ruby
136
+ def my_method
137
+ call_other_method
138
+ end
139
+ ```
140
+ It will be represented with the following structure:
141
+
142
+ ```ruby
143
+ ast =
144
+ s(:def, :my_method,
145
+ s(:args),
146
+ s(:send, nil, :call_other_method))
147
+ ```
148
+
149
+ Keep an eye on the node `(args)`.
150
+
151
+ Then you know you can't use `...` but you can match with `(_)` to match with
152
+ such case.
153
+
154
+ Let's test a few other examples. You can go deeply with the arrays. Let's suppose we have a hardcore call to
68
155
  `a.b.c.d` and the following AST represents it:
69
156
 
70
157
  ```ruby
@@ -78,7 +165,8 @@ ast =
78
165
  :d)
79
166
  ```
80
167
 
81
- You can search using sub-arrays in the same way:
168
+ You can search using sub-arrays with **pure values**, or **shortcuts** or
169
+ **procs**:
82
170
 
83
171
  ```ruby
84
172
  Fast.match?(ast, [:send, [:send, '...'], :d]) # => true
@@ -86,11 +174,20 @@ Fast.match?(ast, [:send, [:send, '...'], :c]) # => false
86
174
  Fast.match?(ast, [:send, [:send, [:send, '...'], :c], :d]) # => true
87
175
  ```
88
176
 
89
- It also knows how to parse strings:
177
+ Shortcuts like `...` and `_` are just literals for procs. Then you can use
178
+ procs directly too:
90
179
 
91
180
  ```ruby
92
- expression = '(send (send (send (send nil $_) $_) $_) $_)'
93
- Fast.match?(ast, expression)) # => [:a, :b, :c, :d]
181
+ Fast.match?(ast, [:send, [ -> (node) { node.type == :send }, [:send, '...'], :c], :d]) # => true
182
+ ```
183
+
184
+ And also work with expressions:
185
+
186
+ ```ruby
187
+ Fast.match?(
188
+ ast,
189
+ '(send (send (send (send nil $_) $_) $_) $_)'
190
+ ) # => [:a, :b, :c, :d]
94
191
  ```
95
192
 
96
193
  If something does not work you can debug with a block:
@@ -106,22 +203,198 @@ int == (int 1) # => true
106
203
  1 == 1 # => true
107
204
  ```
108
205
 
109
- ## Search in files
206
+ ## Use previous captures in search
207
+
208
+ Imagine you're looking for a method that is just delegating something to
209
+ another method, like:
210
+
211
+ ```ruby
212
+ def name
213
+ person.name
214
+ end
215
+ ```
216
+
217
+ This can be represented as the following AST:
218
+
219
+ ```
220
+ (def :name
221
+ (args)
222
+ (send
223
+ (send nil :person) :name))
224
+ ```
225
+
226
+ Then, let's build a search for methods that calls an attribute with the same
227
+ name:
228
+
229
+ ```ruby
230
+ Fast.match?(ast,'(def $_ ... (send (send nil _) \1))') # => [:name]
231
+ ```
232
+
233
+ ### Fast.search
234
+
235
+ Search allows you to go deeply in the AST, collecting nodes that matches with
236
+ the expression. It also returns captures if they exist.
237
+
238
+ ```ruby
239
+ Fast.search(code('a = 1'), '(int _)') # => s(:int, 1)
240
+ ```
241
+
242
+ If you use captures, it returns the node and the captures respectively:
243
+
244
+ ```ruby
245
+ Fast.search(code('a = 1'), '(int $_)') # => [s(:int, 1), 1]
246
+ ```
247
+
248
+ ### Fast.capture
249
+
250
+ To pick just the captures and ignore the nodes, use `Fast.capture`:
251
+
252
+ ```ruby
253
+ Fast.capture(code('a = 1'), '(int $_)') # => 1
254
+ ```
255
+ ### Fast.replace
256
+
257
+ And if I want to refactor a code and use `delegate <attribute>, to: <object>`, try with replace:
258
+
259
+ ```ruby
260
+ Fast.replace ast,
261
+ '(def $_ ... (send (send nil $_) \1))',
262
+ -> (node, captures) {
263
+ attribute, object = captures
264
+ replace(
265
+ node.location.expression,
266
+ "delegate :#{attribute}, to: :#{object}"
267
+ )
268
+ }
269
+ ```
270
+
271
+ ### Replacing file
272
+
273
+ Now let's imagine we have real files like `sample.rb` with the following code:
274
+
275
+ ```ruby
276
+ def good_bye
277
+ message = ["good", "bye"]
278
+ puts message.join(' ')
279
+ end
280
+ ```
281
+
282
+ And we decide to remove the `message` variable and put it inline with the `puts`.
283
+
284
+ Basically, we need to find the local variable assignment, store the value in
285
+ memory. Remove the assignment expression and use the value where the variable
286
+ is being called.
287
+
288
+ ```ruby
289
+ assignment = nil
290
+ Fast.replace_file('sample.rb', '({ lvasgn lvar } message )',
291
+ -> (node, _) {
292
+ if node.type == :lvasgn
293
+ assignment = node.children.last
294
+ remove(node.location.expression)
295
+ elsif node.type == :lvar
296
+ replace(node.location.expression, assignment.location.expression.source)
297
+ end
298
+ }
299
+ )
300
+ ```
301
+
302
+ ## Other useful functions
303
+
304
+ To manipulate ruby files, some times you'll need some extra tasks.
305
+
306
+ ### Fast.ast_from_File(file)
307
+
308
+ This method parses the code and load into a AST representation.
309
+
310
+ ```ruby
311
+ Fast.ast_from_file('sample.rb')
312
+ ```
313
+
314
+ ### Fast.search_file
315
+
316
+ You can use `search_file` and pass the path for search for expressions inside
317
+ files.
318
+
319
+ ```ruby
320
+ Fast.search_file('file.rb', expression)
321
+ ```
322
+
323
+ It's simple combination of `Fast.ast_from_file` with `Fast.search`.
324
+
325
+ ### Fast.ruby_files_from(arguments)
326
+
327
+ You'll be probably looking for multiple ruby files, then this method fetches
328
+ all internal `.rb` files
329
+
330
+ ```ruby
331
+ Fast.ruby_files_from(['lib']) # => ["lib/fast.rb"]
332
+ ```
333
+
334
+ ## `fast` in the command line
110
335
 
111
336
  It will also inject a executable named `fast` and you can use it to search and
112
- find code by this kind of expression
337
+ find code using the concept:
113
338
 
114
339
  ```
115
- $ fast '(:def :match_node? _ )' lib/fast.rb 20:36:21
340
+ $ fast '(def match?)' lib/fast.rb
116
341
  ```
117
342
 
118
343
  - Use `-d` or `--debug` for enable debug mode.
119
344
  - Use `--ast` to output the AST instead of the original code
120
345
 
346
+ ## Experiments
347
+
348
+ You can define experiments and build experimental research to improve some code in
349
+ an automated way.
350
+
351
+ Let's create an experiment to try to remove `before` or `after` blocks
352
+ and run specs. If the spec pass without need the hook, the hook is useless.
353
+
354
+ ```ruby
355
+ Fast.experiment("RSpec/RemoveUselessBeforeAfterHook") do
356
+ lookup 'spec'
357
+ search "(block (send nil {before after}))"
358
+ edit {|node| remove(node.loc.expression) }
359
+ policy {|new_file| system("bin/spring rspec --fail-fast #{new_file}") }
360
+ end
361
+ ```
362
+
363
+ - In the `lookup` you can pass files or folders.
364
+ - The `search` contains the expression you want to match
365
+ - With `edit` block you can apply the code change
366
+ - And the `policy` is executed to check if the current change is valuable
367
+
368
+ If the file contains multiple `before` or `after` blocks, each removal will
369
+ occur independently and the successfull removals will be combined as a
370
+ secondary change. The process repeates until find all possible combinations.
371
+
372
+ See more examples in [experiments](experiments) folder.
373
+
374
+ To run multiple experiments, use `fast-experiment` runner:
375
+
376
+ ```
377
+ fast-experiment <experiment-names> <files-or-folders>
378
+ ```
379
+
380
+ You can limit experiments or file escope:
381
+
382
+ ```
383
+ fast-experiment RSpec/RemoveUselessBeforeAfterHook spec/models/**/*_spec.rb
384
+ ```
385
+
121
386
  ## Development
122
387
 
123
388
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
124
389
 
390
+ On the console we have a few functions like `s` and `code` to make it easy ;)
391
+
392
+ $ bin/console
393
+
394
+ ```ruby
395
+ code("a = 1") # => s(:lvasgn, s(:int, 1))
396
+ ```
397
+
125
398
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
126
399
 
127
400
  ## Contributing