ast_transform 1.0.0.pre.alpha.pre.15
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 +7 -0
- data/.gitignore +53 -0
- data/.travis.yml +18 -0
- data/CHANGELOG.md +24 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +69 -0
- data/LICENSE.txt +21 -0
- data/README.md +185 -0
- data/Rakefile +16 -0
- data/ast_transform.gemspec +43 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/ast_transform/abstract_transformation.rb +37 -0
- data/lib/ast_transform/instruction_sequence/bootsnap_mixin.rb +21 -0
- data/lib/ast_transform/instruction_sequence/mixin.rb +21 -0
- data/lib/ast_transform/instruction_sequence/mixin_utils.rb +12 -0
- data/lib/ast_transform/instruction_sequence.rb +36 -0
- data/lib/ast_transform/source_map.rb +232 -0
- data/lib/ast_transform/transformation.rb +110 -0
- data/lib/ast_transform/transformation_helper.rb +17 -0
- data/lib/ast_transform/transformer.rb +113 -0
- data/lib/ast_transform/version.rb +3 -0
- data/lib/ast_transform.rb +43 -0
- metadata +178 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 48a95ba22654bc424c664e918b4d05d96938aa9647adfa742de94f9fb21ea6ce
|
|
4
|
+
data.tar.gz: 42611d110dbf44a11603d05b83b1eadfafb49fb0733dbb1063ec0f204af0fa3a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 84cd1fb8c9a63f0f565cc1cf6bf2cf75e10d0d6e8920372049f147aa6ddb7cc48e1324178a3358993bb826521d29bdb80ff7d7095a4150f6cf2d6ec71fda024b
|
|
7
|
+
data.tar.gz: a6a1411fbcd95e50ae9834d82a29093707feaac26e604364bd04e4e8c7f0510db44a8b205ac67422bb33e90ad9d11c7bb78dbcb1cb941715e27fb7255e6e87a6
|
data/.gitignore
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
*.gem
|
|
2
|
+
*.rbc
|
|
3
|
+
/.config
|
|
4
|
+
/coverage/
|
|
5
|
+
/InstalledFiles
|
|
6
|
+
/pkg/
|
|
7
|
+
/spec/reports/
|
|
8
|
+
/spec/examples.txt
|
|
9
|
+
/test/tmp/
|
|
10
|
+
/test/version_tmp/
|
|
11
|
+
/tmp/
|
|
12
|
+
|
|
13
|
+
# Used by dotenv library to load environment variables.
|
|
14
|
+
# .env
|
|
15
|
+
|
|
16
|
+
## Specific to RubyMotion:
|
|
17
|
+
.dat*
|
|
18
|
+
.repl_history
|
|
19
|
+
build/
|
|
20
|
+
*.bridgesupport
|
|
21
|
+
build-iPhoneOS/
|
|
22
|
+
build-iPhoneSimulator/
|
|
23
|
+
|
|
24
|
+
## Specific to RubyMotion (use of CocoaPods):
|
|
25
|
+
#
|
|
26
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
|
27
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
|
28
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
|
29
|
+
#
|
|
30
|
+
# vendor/Pods/
|
|
31
|
+
|
|
32
|
+
## Documentation cache and generated files:
|
|
33
|
+
/.yardoc/
|
|
34
|
+
/_yardoc/
|
|
35
|
+
/doc/
|
|
36
|
+
/rdoc/
|
|
37
|
+
|
|
38
|
+
## Environment normalization:
|
|
39
|
+
/.bundle/
|
|
40
|
+
/vendor/bundle
|
|
41
|
+
/lib/bundler/man/
|
|
42
|
+
|
|
43
|
+
# for a library or gem, you might want to ignore these files since the code is
|
|
44
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
45
|
+
# Gemfile.lock
|
|
46
|
+
# .ruby-version
|
|
47
|
+
# .ruby-gemset
|
|
48
|
+
|
|
49
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
|
50
|
+
.rvmrc
|
|
51
|
+
|
|
52
|
+
# RubyMine
|
|
53
|
+
/.idea
|
data/.travis.yml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
language: ruby
|
|
2
|
+
rvm:
|
|
3
|
+
- '2.2'
|
|
4
|
+
- '2.3'
|
|
5
|
+
- '2.4'
|
|
6
|
+
- '2.5'
|
|
7
|
+
- '2.6'
|
|
8
|
+
- '2.7'
|
|
9
|
+
install: ./bin/setup
|
|
10
|
+
deploy:
|
|
11
|
+
provider: rubygems
|
|
12
|
+
gemspec: ast_transform.gemspec
|
|
13
|
+
gem: ast_transform
|
|
14
|
+
on:
|
|
15
|
+
branch: master
|
|
16
|
+
tag: true
|
|
17
|
+
api_key:
|
|
18
|
+
secure: a+D7+wUWmgdk39C+oDFAqjLDAE9s8uaN08xPgf0/IaxFy3PGeuxKSi+3ZFaB0f6zI203mMe7m0sNVD77PxyPqKToKal5K9240+m9yZGoS4seMOJExcBmhiMXk8KOj9upw5Fk0PY91LY5lU13m6u40MtHT9r8da8AysO21PGhkrDkYZNGZ/0nCpV3hnux6DOlNguUhHuuKIKCY6xN9tldwUhVZJvDjSKLe0aRMbP0PZhM3JzMz9BfosirkdE9u666ryI5kIFfHgzDCekiU1sbEugOKyHyu90kaVhgRAXA81LHirj6U9pA0cfJOq84EAE2di6A0xswlbM8GBXdJpRIh706A6sZ7ByYG2SlxyF9vHogqhpg8aU5IMk+sNbrg6++qyTW8zSTksoJN/kMAHcEvMloK4Ja2rRPqhIJkawcQRd5unxHnt4jc6ED2ryDbXhAaBWX4G80jpg5lSeHIQH/S9v+PWcG9UB8Rv08oiJadEid+26r4rWsRA0f/hBP6K+KMq3rACa2Xapgo9ZbSE4xBBu+Yc+4ooiKQ42y+10A5TlTI6iSxKUDcmbH/qVZIchzlDv4MFYoE50BHVn9p0J6dbf8q+WQXsRi1ojEcVy95hd00wdmeMGItE7VuW8zDObJ5W8LNaKuKD358o+4v5CrammfNNz/MynRXhfmPCwyy8s=
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
All notable changes to this project will be documented in this file.
|
|
3
|
+
|
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.4] 2019-06-20
|
|
8
|
+
### Fixed
|
|
9
|
+
- Source mapping for transformations wrapping source nodes into virtual nodes now work.
|
|
10
|
+
|
|
11
|
+
## [0.1.3] 2019-05-28
|
|
12
|
+
### Added
|
|
13
|
+
- Changing the output path for transformed files is now supported through `ASTTransform.output_path=`.
|
|
14
|
+
|
|
15
|
+
## [0.1.2] 2018-12-21
|
|
16
|
+
### Fixed
|
|
17
|
+
- Bumped and relaxed Unparser dependency to ~> 0.4
|
|
18
|
+
|
|
19
|
+
## [0.1.1] 2018-12-06
|
|
20
|
+
### Added
|
|
21
|
+
- ASTTransform::Transformer now supports passing a custom AST::Node Builder.
|
|
22
|
+
|
|
23
|
+
## [0.1.0] 2018-11-08
|
|
24
|
+
### Initial Release!
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
ast_transform (1.0.0)
|
|
5
|
+
parser (~> 2.7)
|
|
6
|
+
unparser (~> 0.4)
|
|
7
|
+
|
|
8
|
+
GEM
|
|
9
|
+
remote: https://rubygems.org/
|
|
10
|
+
specs:
|
|
11
|
+
abstract_type (0.0.7)
|
|
12
|
+
adamantium (0.2.0)
|
|
13
|
+
ice_nine (~> 0.11.0)
|
|
14
|
+
memoizable (~> 0.4.0)
|
|
15
|
+
ansi (1.5.0)
|
|
16
|
+
ast (2.4.1)
|
|
17
|
+
builder (3.2.4)
|
|
18
|
+
byebug (11.1.3)
|
|
19
|
+
coderay (1.1.3)
|
|
20
|
+
concord (0.1.5)
|
|
21
|
+
adamantium (~> 0.2.0)
|
|
22
|
+
equalizer (~> 0.0.9)
|
|
23
|
+
diff-lcs (1.4.4)
|
|
24
|
+
equalizer (0.0.11)
|
|
25
|
+
ice_nine (0.11.2)
|
|
26
|
+
memoizable (0.4.2)
|
|
27
|
+
thread_safe (~> 0.3, >= 0.3.1)
|
|
28
|
+
method_source (1.0.0)
|
|
29
|
+
minitest (5.14.1)
|
|
30
|
+
minitest-reporters (1.4.2)
|
|
31
|
+
ansi
|
|
32
|
+
builder
|
|
33
|
+
minitest (>= 5.0)
|
|
34
|
+
ruby-progressbar
|
|
35
|
+
parser (2.7.1.4)
|
|
36
|
+
ast (~> 2.4.1)
|
|
37
|
+
procto (0.0.3)
|
|
38
|
+
pry (0.13.1)
|
|
39
|
+
coderay (~> 1.1)
|
|
40
|
+
method_source (~> 1.0)
|
|
41
|
+
pry-byebug (3.9.0)
|
|
42
|
+
byebug (~> 11.0)
|
|
43
|
+
pry (~> 0.13.0)
|
|
44
|
+
rake (13.0.1)
|
|
45
|
+
ruby-progressbar (1.10.1)
|
|
46
|
+
thread_safe (0.3.6)
|
|
47
|
+
unparser (0.4.7)
|
|
48
|
+
abstract_type (~> 0.0.7)
|
|
49
|
+
adamantium (~> 0.2.0)
|
|
50
|
+
concord (~> 0.1.5)
|
|
51
|
+
diff-lcs (~> 1.3)
|
|
52
|
+
equalizer (~> 0.0.9)
|
|
53
|
+
parser (>= 2.6.5)
|
|
54
|
+
procto (~> 0.0.2)
|
|
55
|
+
|
|
56
|
+
PLATFORMS
|
|
57
|
+
ruby
|
|
58
|
+
|
|
59
|
+
DEPENDENCIES
|
|
60
|
+
ast_transform!
|
|
61
|
+
bundler (~> 2.1)
|
|
62
|
+
minitest (~> 5.14)
|
|
63
|
+
minitest-reporters (~> 1.4.2)
|
|
64
|
+
pry
|
|
65
|
+
pry-byebug
|
|
66
|
+
rake (~> 13.0)
|
|
67
|
+
|
|
68
|
+
BUNDLED WITH
|
|
69
|
+
2.1.4
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Jean-Philippe Duchesne
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
[](https://travis-ci.org/rspockframework/ast-transform)
|
|
2
|
+
|
|
3
|
+
# ASTTransform
|
|
4
|
+
|
|
5
|
+
ASTTransform is an Abstract Syntax Tree (AST) transformation framework. It hooks into the compilation process and allows to perform AST transformations using an annotation: `transform!`.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'ast_transform'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
$ bundle
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
$ gem install ast_transform
|
|
22
|
+
|
|
23
|
+
Add this to the very beginning of your script or application to install the ASTTransform hook:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
require 'ast_transform'
|
|
27
|
+
ASTTransform.install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Compatibility with Bootsnap
|
|
31
|
+
|
|
32
|
+
ASTTransform is compatible with [Bootsnap](https://github.com/Shopify/bootsnap/). The only requirement is to install the above hook after Bootsnap, and ASTTransform does the rest for you.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
Getting started using ASTTransform is extremely easy! All you need is to use the `transform!` annotation:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
transform!(MyTransformation)
|
|
40
|
+
class MyClass
|
|
41
|
+
# ...
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
When your class is required and loaded into the runtime, ASTTransform will run the `MyTransformation` transformation on the annotated code.
|
|
46
|
+
|
|
47
|
+
### Supported annotated code
|
|
48
|
+
|
|
49
|
+
The following expressions can be annotated, which will pass only the annotated AST node to the transformation:
|
|
50
|
+
|
|
51
|
+
#### Class definitions
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
transform!(MyTransformation)
|
|
55
|
+
class Foo
|
|
56
|
+
# ...
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
#### Constant assignments
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
transform!(MyTransformation)
|
|
64
|
+
Foo = Class.new do
|
|
65
|
+
# ...
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Running multiple transformations
|
|
70
|
+
|
|
71
|
+
#### On the same AST node
|
|
72
|
+
|
|
73
|
+
You can run multiple transformations on the same code, by passing multiple transformations to the annotation:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
transform!(MyTransformation1, MyTransformation2)
|
|
77
|
+
class Foo
|
|
78
|
+
# ...
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Note**: The transformations will be executed in order, the output of the previous transformation being fed into the next, etc...
|
|
83
|
+
|
|
84
|
+
#### On different AST nodes
|
|
85
|
+
|
|
86
|
+
Because each `transform!` annotation runs transformations in isolated scope, it is possible to have multiple annotated nodes in the same file:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
transform!(MyTransformation)
|
|
90
|
+
class Foo
|
|
91
|
+
# ...
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
transform!(MyTransformation)
|
|
95
|
+
class Bar
|
|
96
|
+
# ...
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
You can even have nested `transform!` annotations:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
transform!(FooTransformation)
|
|
104
|
+
class Foo
|
|
105
|
+
transform!(BarTransformation)
|
|
106
|
+
class Bar
|
|
107
|
+
# ...
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# ...
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The above code would first process class `Foo` using `FooTransformation` (which could even make modifications to `Bar` on its own), and then `BarTransformation` would be run against `Bar`.
|
|
115
|
+
|
|
116
|
+
### Writing Transformations
|
|
117
|
+
|
|
118
|
+
For more in-depth information regarding processing AST nodes, we recommend looking at https://github.com/whitequark/ast, as transformations are built on top of `Parser::AST::Processor`, which in turn is built on top of the `ast` gem.
|
|
119
|
+
|
|
120
|
+
Transformations should derive from `ASTTransform::AbstractTransformation`:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
require 'ast_transform/abstract_transformation'
|
|
124
|
+
|
|
125
|
+
class MyTransformation < ASTTransformation::AbstractTransformation
|
|
126
|
+
# ...
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### Transformation discoverability
|
|
131
|
+
|
|
132
|
+
ASTTransform automatically loads your transformations at compile time. As such, we expect your files to be located at a known path.
|
|
133
|
+
|
|
134
|
+
Transformations are required using the following scheme, i.e. for `MyNamespace::MyTransformation`, it will make the following call, so your file must be placed accordingly for ASTTransform to find it:
|
|
135
|
+
```ruby
|
|
136
|
+
require 'my_namespace/my_transformation'
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### Processing each node
|
|
140
|
+
|
|
141
|
+
To do some processing on each node, override the `process_node` private method. If you do this, make sure to also process the children nodes if required.
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
require 'ast_transform/abstract_transformation'
|
|
145
|
+
|
|
146
|
+
class MyTransformation < ASTTransformation::AbstractTransformation
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def process_node(node)
|
|
150
|
+
# ... processing
|
|
151
|
+
node.updated(nil, process_all(node.children))
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
In the above, `node#updated` allows updating the node, either its type or its children. Each node is immutable, so updating nodes requires recursively re-creating the tree from the deepest modified nodes. Passing `nil` as the first argument keeps the same node type.
|
|
157
|
+
|
|
158
|
+
#### Processing on certain types of nodes only
|
|
159
|
+
|
|
160
|
+
The [ast gem](https://github.com/whitequark/ast) uses a pattern in which a Transformation may implement a method matching a node type, i.e. `on_class`, `on_send`, `on_lvar`, etc... This is very useful when transformations should process all nodes of this type.
|
|
161
|
+
|
|
162
|
+
### Parameterizable transformations
|
|
163
|
+
|
|
164
|
+
If you want your transformation to be customizable, accept the parameters in the constructor. The annotation can the be changed accordingly:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
class FooTransformation < ASTTransform::AbstractTransformation
|
|
168
|
+
def initialize(param1, params2: false)
|
|
169
|
+
# ...
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# ...
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
transform!(FooTransformation.new(param1, param2: true))
|
|
176
|
+
class Foo
|
|
177
|
+
# ...
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Development
|
|
182
|
+
|
|
183
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
184
|
+
|
|
185
|
+
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).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'bundler/gem_tasks'
|
|
3
|
+
require 'rake/testtask'
|
|
4
|
+
|
|
5
|
+
Rake::TestTask.new(:test) do |t|
|
|
6
|
+
# Ensure we load test_loader first for ASTTransform.install
|
|
7
|
+
filepath = File.expand_path('test/test_loader.rb', __dir__)
|
|
8
|
+
t.ruby_opts << "-r #{filepath}"
|
|
9
|
+
t.warning = false
|
|
10
|
+
|
|
11
|
+
t.libs << 'test'
|
|
12
|
+
t.libs << 'lib'
|
|
13
|
+
t.test_files = FileList['test/**/*_test.rb']
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
task :default => :test
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
|
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require "ast_transform/version"
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "ast_transform"
|
|
8
|
+
spec.version = ASTTransform::VERSION
|
|
9
|
+
spec.authors = ["Jean-Philippe Duchesne"]
|
|
10
|
+
spec.email = ["jpduchesne89@gmail.com"]
|
|
11
|
+
|
|
12
|
+
spec.summary = 'An AST transformation framework.'
|
|
13
|
+
spec.description = spec.summary
|
|
14
|
+
spec.homepage = "https://github.com/rspockframework/ast-transform"
|
|
15
|
+
spec.license = "MIT"
|
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
|
18
|
+
end
|
|
19
|
+
spec.bindir = "exe"
|
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
21
|
+
spec.require_paths = ["lib"]
|
|
22
|
+
spec.required_ruby_version = '~> 2.2'
|
|
23
|
+
|
|
24
|
+
if ENV['TRAVIS']
|
|
25
|
+
if ENV['TRAVIS_TAG'].nil? || ENV['TRAVIS_TAG'].empty?
|
|
26
|
+
spec.version = "#{spec.version}-alpha-#{ENV['TRAVIS_BUILD_NUMBER']}"
|
|
27
|
+
elsif ENV['TRAVIS_TAG'] != spec.version.to_s
|
|
28
|
+
raise "Tag name (#{ENV['TRAVIS_TAG']}) and Gem version (#{spec.version}) are different"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Development dependencies
|
|
33
|
+
spec.add_development_dependency "bundler", "~> 2.1"
|
|
34
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
35
|
+
spec.add_development_dependency "minitest", "~> 5.14"
|
|
36
|
+
spec.add_development_dependency "minitest-reporters", "~> 1.4.2"
|
|
37
|
+
spec.add_development_dependency "pry"
|
|
38
|
+
spec.add_development_dependency "pry-byebug"
|
|
39
|
+
|
|
40
|
+
# Runtime dependencies
|
|
41
|
+
spec.add_runtime_dependency "parser", "~> 2.7"
|
|
42
|
+
spec.add_runtime_dependency "unparser", "~> 0.4"
|
|
43
|
+
end
|
data/bin/console
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "ast_transform"
|
|
5
|
+
|
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
+
|
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
+
# require "pry"
|
|
11
|
+
# Pry.start
|
|
12
|
+
|
|
13
|
+
require "irb"
|
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'ast_transform/transformation_helper'
|
|
3
|
+
|
|
4
|
+
module ASTTransform
|
|
5
|
+
class AbstractTransformation < Parser::AST::Processor
|
|
6
|
+
include TransformationHelper
|
|
7
|
+
|
|
8
|
+
# Runs this transformation on +node+.
|
|
9
|
+
# Note: If you want to add one-time checks to the transformation, override this, then call super.
|
|
10
|
+
#
|
|
11
|
+
# @param node [Parser::AST::Node] The node to be transformed.
|
|
12
|
+
#
|
|
13
|
+
# @return [Parser::AST::Node] The transformed node.
|
|
14
|
+
def run(node)
|
|
15
|
+
process(node)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Used internally by Parser::AST::Processor to process each node. DO NOT OVERRIDE.
|
|
19
|
+
def process(node)
|
|
20
|
+
return node unless node.is_a?(Parser::AST::Node)
|
|
21
|
+
|
|
22
|
+
process_node(node)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
# Processes the given +node+.
|
|
28
|
+
# Note: If you want to do processing on each node, override this.
|
|
29
|
+
#
|
|
30
|
+
# @param node [Parser::AST::Node] The node to be transformed.
|
|
31
|
+
#
|
|
32
|
+
# @return [Parser::AST::Node] The transformed node.
|
|
33
|
+
def process_node(node)
|
|
34
|
+
method(:process).super_method.call(node)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ast_transform/transformer'
|
|
4
|
+
require 'ast_transform/instruction_sequence/mixin_utils'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
|
|
7
|
+
module ASTTransform
|
|
8
|
+
module InstructionSequence
|
|
9
|
+
module BootsnapMixin
|
|
10
|
+
def input_to_storage(source, source_path)
|
|
11
|
+
return ASTTransform::MixinUtils.try_super(self, :input_to_storage, source, source_path) if source_path == __FILE__
|
|
12
|
+
return ASTTransform::MixinUtils.try_super(self, :input_to_storage, source, source_path) unless source =~ /transform!/
|
|
13
|
+
|
|
14
|
+
iseq = ASTTransform::InstructionSequence.source_to_transformed_iseq(source, source_path)
|
|
15
|
+
iseq.to_binary
|
|
16
|
+
rescue SyntaxError
|
|
17
|
+
raise ::Bootsnap::CompileCache::Uncompilable, 'syntax error'
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'pathname'
|
|
3
|
+
require 'ast_transform/transformer'
|
|
4
|
+
require 'ast_transform/transformation'
|
|
5
|
+
require 'ast_transform/instruction_sequence/mixin_utils'
|
|
6
|
+
|
|
7
|
+
module ASTTransform
|
|
8
|
+
module InstructionSequence
|
|
9
|
+
module Mixin
|
|
10
|
+
def load_iseq(source_path)
|
|
11
|
+
return ASTTransform::MixinUtils.try_super(self, :load_iseq, source_path) if source_path == __FILE__
|
|
12
|
+
|
|
13
|
+
source = File.read(source_path)
|
|
14
|
+
|
|
15
|
+
return ASTTransform::MixinUtils.try_super(self, :load_iseq, source_path) unless source =~ /transform!/
|
|
16
|
+
|
|
17
|
+
ASTTransform::InstructionSequence.source_to_transformed_iseq(source, source_path)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ASTTransform
|
|
4
|
+
module MixinUtils
|
|
5
|
+
class << self
|
|
6
|
+
def try_super(target, method_sym, *args, &block)
|
|
7
|
+
super_method = target.method(method_sym).super_method
|
|
8
|
+
super_method ? super_method.call(*args, &block) : nil
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ASTTransform
|
|
3
|
+
module InstructionSequence
|
|
4
|
+
class << self
|
|
5
|
+
def using_bootsnap_compilation?
|
|
6
|
+
filepath, = RubyVM::InstructionSequence.method(:load_iseq).source_location
|
|
7
|
+
filepath =~ %r{/bootsnap/}
|
|
8
|
+
rescue NameError
|
|
9
|
+
false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def source_to_transformed_iseq(source, source_path)
|
|
13
|
+
transformer = ASTTransform::Transformer.new(ASTTransform::Transformation.new)
|
|
14
|
+
rewritten_file_pathname = write_pathname(source_path)
|
|
15
|
+
|
|
16
|
+
rewritten_source = transformer.transform_file_source(source, source_path, rewritten_file_pathname.to_s)
|
|
17
|
+
write(rewritten_source, rewritten_file_pathname)
|
|
18
|
+
|
|
19
|
+
RubyVM::InstructionSequence.compile(rewritten_source, rewritten_file_pathname.to_s, rewritten_file_pathname.to_s)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def write_pathname(file_path)
|
|
23
|
+
project_path = File.expand_path("")
|
|
24
|
+
relative_source_file_pathname = Pathname.new(file_path).relative_path_from(Pathname.new(project_path))
|
|
25
|
+
Pathname.new("").join(project_path, 'tmp', 'ast_transform', relative_source_file_pathname)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def write(string, pathname)
|
|
29
|
+
FileUtils.mkdir_p(pathname.dirname)
|
|
30
|
+
File.open(pathname, 'w') do |file|
|
|
31
|
+
file.write(string)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'parser/current'
|
|
3
|
+
|
|
4
|
+
module ASTTransform
|
|
5
|
+
class SourceMap
|
|
6
|
+
class << self
|
|
7
|
+
# Registers the given SourceMap.
|
|
8
|
+
#
|
|
9
|
+
# @param source_map [SourceMap] The source map to be registered.
|
|
10
|
+
#
|
|
11
|
+
# @return [void]
|
|
12
|
+
def register_source_map(source_map)
|
|
13
|
+
source_maps[source_map.transformed_file_path] = source_map
|
|
14
|
+
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Retrieves the SourceMap for the given +file_path+.
|
|
19
|
+
#
|
|
20
|
+
# @param file_path [String] The transformed file path.
|
|
21
|
+
#
|
|
22
|
+
# @return [SourceMap|nil] The associated source map.
|
|
23
|
+
def for_file_path(file_path)
|
|
24
|
+
source_maps[file_path]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def source_maps
|
|
30
|
+
@@source_maps ||= {}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Constructs a new SourceMap instance.
|
|
35
|
+
#
|
|
36
|
+
# Note: +source_ranges_ast+ and +transformed_ranges_ast+ must be equivalent ASTs.
|
|
37
|
+
#
|
|
38
|
+
# @param source_file_path [String] The path to the source file.
|
|
39
|
+
# @param transformed_file_path [String] The path to the transformed file.
|
|
40
|
+
# @param source_ranges_ast [Parser::AST::Node] A transformed AST that contains the source code ranges.
|
|
41
|
+
# @param transformed_ranges_ast [Parser::AST::Node] A transformed AST that contains the ranges for the executed
|
|
42
|
+
# code.
|
|
43
|
+
def initialize(source_file_path, transformed_file_path, source_ranges_ast, transformed_ranges_ast)
|
|
44
|
+
@source_file_path = source_file_path
|
|
45
|
+
@transformed_file_path = transformed_file_path
|
|
46
|
+
@source_ranges_ast = source_ranges_ast
|
|
47
|
+
@transformed_ranges_ast = transformed_ranges_ast
|
|
48
|
+
|
|
49
|
+
@lines = Hash.new { |hash, key| hash[key] = [] }
|
|
50
|
+
extract_source_map_data(@transformed_ranges_ast, [])
|
|
51
|
+
@source_map = build_source_map.freeze
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
attr_reader :source_file_path, :transformed_file_path, :source_map
|
|
55
|
+
|
|
56
|
+
# Retrieves the mapped line number for the given +line_number+.
|
|
57
|
+
#
|
|
58
|
+
# @param line_number [Integer] The line number in the executed code to be mapped to the source.
|
|
59
|
+
#
|
|
60
|
+
# @return [Integer|nil] The mapped line number, otherwise nil if not found.
|
|
61
|
+
def line(line_number)
|
|
62
|
+
@source_map[line_number]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Retrieves the line count for the executed code.
|
|
66
|
+
#
|
|
67
|
+
# @return [Integer] The line count.
|
|
68
|
+
def line_count
|
|
69
|
+
@transformed_ranges_ast&.loc&.expression.last_line || 0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Extracts SourceMap data from the given node.
|
|
75
|
+
#
|
|
76
|
+
# @param node [Parser::AST::Node] The node containing ranges for the executed code.
|
|
77
|
+
#
|
|
78
|
+
# @return [void]
|
|
79
|
+
def extract_source_map_data(node, indexes)
|
|
80
|
+
return false unless node&.is_a?(Parser::AST::Node)
|
|
81
|
+
|
|
82
|
+
range = node.loc&.expression
|
|
83
|
+
|
|
84
|
+
if range && range.line == range.last_line
|
|
85
|
+
@lines[range.line] << indexes.dup
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
node.children.each.with_index do |child, index|
|
|
89
|
+
extract_source_map_data(child, indexes.dup << index)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Builds the source map.
|
|
96
|
+
#
|
|
97
|
+
# @return [Hash] A Hash containing line numbers from executed code to source code.
|
|
98
|
+
def build_source_map
|
|
99
|
+
(1..line_count).each.with_object({}) {|it, hash| hash[it] = source_line(it) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Retrieves the source line for the given +line_number+ in the executed code.
|
|
103
|
+
#
|
|
104
|
+
# @param line_number [Integer] The line number in the executed code.
|
|
105
|
+
#
|
|
106
|
+
# @return [Integer|nil] The line number in the source code, or nil if cannot be mapped.
|
|
107
|
+
def source_line(line_number)
|
|
108
|
+
if @lines.key?(line_number)
|
|
109
|
+
@lines[line_number].each do |dig_array|
|
|
110
|
+
source_node = approximate_dig_last_valid_node(@source_ranges_ast, dig_array)
|
|
111
|
+
next unless source_node
|
|
112
|
+
|
|
113
|
+
range = search_range(source_node, 1)
|
|
114
|
+
return range.line if range
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Recursively look for node represented by +indexes+ in +node+. If not found, goes back +depth+ nodes and search for
|
|
122
|
+
# the node pointed to by +indexes+.
|
|
123
|
+
#
|
|
124
|
+
# @param node [Parser::AST::Node] The node to search into. This must be a node in the +@source_ranges_ast+.
|
|
125
|
+
# @param indexes [Array<Integer>] Child indexes pointing to the node we're looking for in +node+.
|
|
126
|
+
# @param depth [Integer] Number of nodes to go up to search for the node pointed to by +indexes+.
|
|
127
|
+
#
|
|
128
|
+
# @return [Parser::AST::Node|nil] The node found, nil otherwise.
|
|
129
|
+
def approximate_dig_last_valid_node(node, indexes, depth = 1)
|
|
130
|
+
return node if indexes.empty?
|
|
131
|
+
|
|
132
|
+
result = dig_node(node, indexes)
|
|
133
|
+
return result if result.is_a?(Parser::AST::Node) || depth <= 0
|
|
134
|
+
|
|
135
|
+
queried_node = dig_last_valid_node(@transformed_ranges_ast, indexes)
|
|
136
|
+
|
|
137
|
+
last_known_index = dig_last_valid_node_index(node, indexes[0...-depth])
|
|
138
|
+
query_indexes = indexes[0...last_known_index]
|
|
139
|
+
|
|
140
|
+
last_known_node = dig_node(node, query_indexes)
|
|
141
|
+
|
|
142
|
+
search_node(last_known_node, queried_node)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Recursively search the children of +node+ for an equivalent +queried_node+.
|
|
146
|
+
#
|
|
147
|
+
# @param node [Parser::AST::Node] The current node to search in.
|
|
148
|
+
# @param queried_node [Parser::AST::Node] The equivalent node to search for.
|
|
149
|
+
#
|
|
150
|
+
# @return [Parser::AST::Node|nil] The found node from the +node+ graph, nil otherwise.
|
|
151
|
+
def search_node(node, queried_node)
|
|
152
|
+
return unless node&.is_a?(Parser::AST::Node)
|
|
153
|
+
return node if node == queried_node
|
|
154
|
+
|
|
155
|
+
node.children.each do |child_node|
|
|
156
|
+
result = search_node(child_node, queried_node)
|
|
157
|
+
return result if result
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
nil
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Recursively search the given +node+ for a range.
|
|
164
|
+
#
|
|
165
|
+
# @param node [Parser::AST::Node] The current node to search in.
|
|
166
|
+
# @param max_range [Integer|nil] The max range to consider valid. Nil means any range is valid. If 1, only ranges
|
|
167
|
+
# which span one line will be considered, etc...
|
|
168
|
+
#
|
|
169
|
+
# @return [Parser::Source::Range|nil] The range, or nil if no range was found. This occurs when the tree contains
|
|
170
|
+
# no ranges, i.e. they're all virtually built nodes.
|
|
171
|
+
def search_range(node, max_range = nil)
|
|
172
|
+
return unless node&.is_a?(Parser::AST::Node)
|
|
173
|
+
|
|
174
|
+
range = node.loc&.expression
|
|
175
|
+
if range && max_range && range.last_line - range.line < max_range || range && max_range.nil?
|
|
176
|
+
return range
|
|
177
|
+
else
|
|
178
|
+
node.children.each do |child_node|
|
|
179
|
+
result = search_range(child_node, max_range)
|
|
180
|
+
return result if result
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
nil
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Finds the index for the last valid node represented by +indexes+ in the children of +node+.
|
|
188
|
+
#
|
|
189
|
+
# @param node [Parser::AST::Node] The current node to search in.
|
|
190
|
+
# @param indexes [Array<Integer>] The array of indexes pointing to the child node to be retrieved from +node+.
|
|
191
|
+
#
|
|
192
|
+
# @return [Integer|nil] The index of the node if found, nil otherwise.
|
|
193
|
+
def dig_last_valid_node_index(node, indexes)
|
|
194
|
+
return if indexes.empty?
|
|
195
|
+
|
|
196
|
+
result = dig_node(node, indexes)
|
|
197
|
+
current_index = indexes&.size
|
|
198
|
+
return current_index if result.is_a?(Parser::AST::Node)
|
|
199
|
+
|
|
200
|
+
dig_last_valid_node_index(node, indexes[0...-1])
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Recursively look for the node represented by +indexes+ in the children of +node+. If not found, returns the last
|
|
204
|
+
# valid node.
|
|
205
|
+
#
|
|
206
|
+
# @param node [Parser::AST::Node] The node to look into.
|
|
207
|
+
# @param indexes [Array<Integer>] The array of indexes pointing to the child node to be retrieved from +node+.
|
|
208
|
+
#
|
|
209
|
+
# @return [Parser::AST::Node|nil] The node if found, nil otherwise.
|
|
210
|
+
def dig_last_valid_node(node, indexes)
|
|
211
|
+
return node if indexes.empty?
|
|
212
|
+
|
|
213
|
+
result = dig_node(node, indexes)
|
|
214
|
+
return result if result.is_a?(Parser::AST::Node)
|
|
215
|
+
|
|
216
|
+
dig_last_valid_node(node, indexes[0...-1])
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Recursively look for the node represented by +indexes+ in the children of +node+.
|
|
220
|
+
#
|
|
221
|
+
# @param node [Parser::AST::Node] The node to look into.
|
|
222
|
+
# @param indexes [Array<Integer>] The array of indexes pointing to the child node to be retrieved from +node+.
|
|
223
|
+
#
|
|
224
|
+
# @return [Parser::AST::Node|nil] The node if found, nil otherwise.
|
|
225
|
+
def dig_node(node, indexes)
|
|
226
|
+
indexes.inject(node) do |node, index|
|
|
227
|
+
return nil unless node.is_a?(Parser::AST::Node)
|
|
228
|
+
node.children[index]
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'ast_transform'
|
|
3
|
+
require 'ast_transform/abstract_transformation'
|
|
4
|
+
require 'ast_transform/transformer'
|
|
5
|
+
require 'unparser'
|
|
6
|
+
|
|
7
|
+
module ASTTransform
|
|
8
|
+
class Transformation < ASTTransform::AbstractTransformation
|
|
9
|
+
TRANSFORM_AST = s(:send, nil, :transform!)
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def process_node(node)
|
|
14
|
+
children = node.children.map.with_index do |child_node, index|
|
|
15
|
+
previous_sibling = previous_child(node, index)
|
|
16
|
+
process_node_helper(child_node, previous_sibling)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
children.reject!.with_index { |child_node, index|
|
|
20
|
+
transform_node?(child_node) && transformable_node?(next_child(node, index))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
node.updated(nil, process_all(children))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def previous_child(node, index)
|
|
27
|
+
index > 0 ? node.children[index - 1] : nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def next_child(node, index)
|
|
31
|
+
index >= 0 ? node.children[index + 1] : nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def process_node_helper(node, previous_node)
|
|
35
|
+
if transform_node?(previous_node) && transformable_node?(node)
|
|
36
|
+
transformations = extract_transformations(previous_node)
|
|
37
|
+
ASTTransform::Transformer.new(*transformations).transform_ast(node)
|
|
38
|
+
else
|
|
39
|
+
node
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def transform_node?(node)
|
|
44
|
+
return false unless node.is_a?(Parser::AST::Node)
|
|
45
|
+
|
|
46
|
+
node.type == :send && node.children.count >= 3 && node.children[0].nil? && node.children[1] == :transform!
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def transformable_node?(node)
|
|
50
|
+
return false unless node.is_a?(Parser::AST::Node)
|
|
51
|
+
|
|
52
|
+
[:class, :casgn].include?(node.type)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def extract_transformations(node)
|
|
56
|
+
node.children.map do |child_node|
|
|
57
|
+
extract_transformation(child_node)
|
|
58
|
+
end.compact!
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def extract_transformation(node)
|
|
62
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
63
|
+
return unless node.children.count >= 2
|
|
64
|
+
|
|
65
|
+
if node.children[1] == :new
|
|
66
|
+
require_transformation(node)
|
|
67
|
+
code = Unparser.unparse(node)
|
|
68
|
+
|
|
69
|
+
TOPLEVEL_BINDING.eval(code)
|
|
70
|
+
else
|
|
71
|
+
require_transformation(node)
|
|
72
|
+
code = "#{Unparser.unparse(node)}.new"
|
|
73
|
+
|
|
74
|
+
TOPLEVEL_BINDING.eval(code)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def require_transformation(node)
|
|
79
|
+
const_node = node.children.first
|
|
80
|
+
const_name = Unparser.unparse(const_node)
|
|
81
|
+
|
|
82
|
+
constant = try_const_get(const_name)
|
|
83
|
+
unless constant
|
|
84
|
+
require_path = require_path(const_name)
|
|
85
|
+
require(require_path)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def require_path(const_name)
|
|
92
|
+
acronyms = ASTTransform.acronyms
|
|
93
|
+
acronym_regex = acronyms.empty? ? /(?=a)b/ : /#{acronyms.join("|")}/
|
|
94
|
+
return const_name unless /[A-Z-]|::/.match?(const_name)
|
|
95
|
+
word = const_name.to_s.gsub("::".freeze, "/".freeze)
|
|
96
|
+
word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'.freeze }#{$2.downcase}" }
|
|
97
|
+
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze)
|
|
98
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze)
|
|
99
|
+
word.tr!("-".freeze, "_".freeze)
|
|
100
|
+
word.downcase!
|
|
101
|
+
word
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def try_const_get(const_name)
|
|
105
|
+
Object.const_get(const_name)
|
|
106
|
+
rescue NameError
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'parser'
|
|
3
|
+
|
|
4
|
+
module ASTTransform
|
|
5
|
+
module TransformationHelper
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(Methods)
|
|
8
|
+
base.include(Methods)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
module Methods
|
|
12
|
+
def s(type, *children, **properties)
|
|
13
|
+
Parser::AST::Node.new(type, children, properties)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'parser/current'
|
|
3
|
+
require 'unparser'
|
|
4
|
+
require 'ast_transform/source_map'
|
|
5
|
+
|
|
6
|
+
module ASTTransform
|
|
7
|
+
class Transformer
|
|
8
|
+
# Constructs a new Transformer instance.
|
|
9
|
+
#
|
|
10
|
+
# @param transformations [Array<ASTTransform::AbstractTransformation>] The transformations to be run.
|
|
11
|
+
# @param builder [Parser::Builders::Default] The AST Node builder.
|
|
12
|
+
def initialize(*transformations, builder: Parser::Builders::Default.new)
|
|
13
|
+
@transformations = transformations
|
|
14
|
+
@builder = builder
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Builds the AST for the given +source+.
|
|
18
|
+
#
|
|
19
|
+
# @param source [String] The input source code.
|
|
20
|
+
# @param file_path [String] The file_path. This is important for source mapping in backtraces.
|
|
21
|
+
#
|
|
22
|
+
# @return [Parser::AST::Node] The AST.
|
|
23
|
+
def build_ast(source, file_path: 'tmp')
|
|
24
|
+
buffer = create_buffer(source, file_path)
|
|
25
|
+
parser.parse(buffer)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Builds the AST for the given +file_path+.
|
|
29
|
+
#
|
|
30
|
+
# @param file_path [String] The input file path.
|
|
31
|
+
#
|
|
32
|
+
# @return [Parser::AST::Node] The AST.
|
|
33
|
+
def build_ast_from_file(file_path)
|
|
34
|
+
source = File.read(file_path)
|
|
35
|
+
build_ast(source, file_path: file_path)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Transforms the given +source+.
|
|
39
|
+
#
|
|
40
|
+
# @param source [String] The input source code to be transformed.
|
|
41
|
+
#
|
|
42
|
+
# @return [String] The transformed code.
|
|
43
|
+
def transform(source)
|
|
44
|
+
ast = build_ast(source)
|
|
45
|
+
transformed_ast = transform_ast(ast)
|
|
46
|
+
Unparser.unparse(transformed_ast)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Transforms the give +file_path+.
|
|
50
|
+
#
|
|
51
|
+
# @param file_path [String] The input file to be transformed. This is required for source mapping in backtraces.
|
|
52
|
+
# @param transformed_file_path [String] The file path to the transformed file.
|
|
53
|
+
#
|
|
54
|
+
# @return [String] The transformed code.
|
|
55
|
+
def transform_file(file_path, transformed_file_path)
|
|
56
|
+
source = File.read(file_path)
|
|
57
|
+
transform_file_source(source, file_path, transformed_file_path)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Transforms the given +source+ in +file_path+.
|
|
61
|
+
#
|
|
62
|
+
# @param source [String] The input source code to be transformed.
|
|
63
|
+
# @param file_path [String] The file path for the input +source+. This is required for source mapping in backtraces.
|
|
64
|
+
# @param transformed_file_path [String] The file path to the transformed filed. This is required to register the
|
|
65
|
+
# SourceMap.
|
|
66
|
+
#
|
|
67
|
+
# @return [String] The transformed code.
|
|
68
|
+
def transform_file_source(source, file_path, transformed_file_path)
|
|
69
|
+
source_ast = build_ast(source, file_path: file_path)
|
|
70
|
+
# At this point, the transformed_ast contains line number mappings for the original +source+.
|
|
71
|
+
transformed_ast = transform_ast(source_ast)
|
|
72
|
+
|
|
73
|
+
transformed_source = Unparser.unparse(transformed_ast)
|
|
74
|
+
|
|
75
|
+
register_source_map(file_path, transformed_file_path, transformed_ast, transformed_source)
|
|
76
|
+
|
|
77
|
+
transformed_source
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Transforms the given +ast+.
|
|
81
|
+
#
|
|
82
|
+
# @param ast [Parser::AST::Node] The input AST to be transformed.
|
|
83
|
+
#
|
|
84
|
+
# @return [Parser::AST::Node] The transformed AST.
|
|
85
|
+
def transform_ast(ast)
|
|
86
|
+
@transformations.inject(ast) do |ast, transformation|
|
|
87
|
+
transformation.run(ast)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def create_buffer(source, file_path)
|
|
94
|
+
buffer = Parser::Source::Buffer.new(file_path)
|
|
95
|
+
buffer.source = source.dup.force_encoding(parser.default_encoding)
|
|
96
|
+
|
|
97
|
+
buffer
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def parser
|
|
101
|
+
@parser&.reset
|
|
102
|
+
@parser ||= Parser::CurrentRuby.new(@builder)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def register_source_map(source_file_path, transformed_file_path, transformed_ast, transformed_source)
|
|
106
|
+
# The transformed_source is re-parsed to get the correct line numbers for the transformed_ast, which is the code
|
|
107
|
+
# that will run.
|
|
108
|
+
rewritten_ast = build_ast(transformed_source)
|
|
109
|
+
source_map = ASTTransform::SourceMap.new(source_file_path, transformed_file_path, transformed_ast, rewritten_ast)
|
|
110
|
+
ASTTransform::SourceMap.register_source_map(source_map)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ast_transform/version"
|
|
4
|
+
require 'ast_transform/instruction_sequence'
|
|
5
|
+
require 'ast_transform/instruction_sequence/mixin'
|
|
6
|
+
require 'ast_transform/instruction_sequence/bootsnap_mixin'
|
|
7
|
+
|
|
8
|
+
module ASTTransform
|
|
9
|
+
DEFAULT_OUTPUT_PATH = Pathname.new("").join("tmp", "ast_transform").to_s
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def acronyms
|
|
13
|
+
@acronyms ||= []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def acronym(acronym)
|
|
17
|
+
acronyms << acronym
|
|
18
|
+
acronyms.uniq!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def install
|
|
22
|
+
@installed ||= begin
|
|
23
|
+
if defined?(Bootsnap) && ASTTransform::InstructionSequence.using_bootsnap_compilation?
|
|
24
|
+
class << Bootsnap::CompileCache::ISeq
|
|
25
|
+
prepend ::ASTTransform::InstructionSequence::BootsnapMixin
|
|
26
|
+
end
|
|
27
|
+
else
|
|
28
|
+
class << RubyVM::InstructionSequence
|
|
29
|
+
prepend ::ASTTransform::InstructionSequence::Mixin
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def output_path=(path)
|
|
36
|
+
@output_path = path
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def output_path
|
|
40
|
+
@output_path || DEFAULT_OUTPUT_PATH
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ast_transform
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0.pre.alpha.pre.15
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jean-Philippe Duchesne
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2020-07-08 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bundler
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.1'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '13.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '13.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: minitest
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '5.14'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '5.14'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: minitest-reporters
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: 1.4.2
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: 1.4.2
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: pry
|
|
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: pry-byebug
|
|
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
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: parser
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '2.7'
|
|
104
|
+
type: :runtime
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '2.7'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: unparser
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0.4'
|
|
118
|
+
type: :runtime
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0.4'
|
|
125
|
+
description: An AST transformation framework.
|
|
126
|
+
email:
|
|
127
|
+
- jpduchesne89@gmail.com
|
|
128
|
+
executables: []
|
|
129
|
+
extensions: []
|
|
130
|
+
extra_rdoc_files: []
|
|
131
|
+
files:
|
|
132
|
+
- ".gitignore"
|
|
133
|
+
- ".travis.yml"
|
|
134
|
+
- CHANGELOG.md
|
|
135
|
+
- Gemfile
|
|
136
|
+
- Gemfile.lock
|
|
137
|
+
- LICENSE.txt
|
|
138
|
+
- README.md
|
|
139
|
+
- Rakefile
|
|
140
|
+
- ast_transform.gemspec
|
|
141
|
+
- bin/console
|
|
142
|
+
- bin/setup
|
|
143
|
+
- lib/ast_transform.rb
|
|
144
|
+
- lib/ast_transform/abstract_transformation.rb
|
|
145
|
+
- lib/ast_transform/instruction_sequence.rb
|
|
146
|
+
- lib/ast_transform/instruction_sequence/bootsnap_mixin.rb
|
|
147
|
+
- lib/ast_transform/instruction_sequence/mixin.rb
|
|
148
|
+
- lib/ast_transform/instruction_sequence/mixin_utils.rb
|
|
149
|
+
- lib/ast_transform/source_map.rb
|
|
150
|
+
- lib/ast_transform/transformation.rb
|
|
151
|
+
- lib/ast_transform/transformation_helper.rb
|
|
152
|
+
- lib/ast_transform/transformer.rb
|
|
153
|
+
- lib/ast_transform/version.rb
|
|
154
|
+
homepage: https://github.com/rspockframework/ast-transform
|
|
155
|
+
licenses:
|
|
156
|
+
- MIT
|
|
157
|
+
metadata: {}
|
|
158
|
+
post_install_message:
|
|
159
|
+
rdoc_options: []
|
|
160
|
+
require_paths:
|
|
161
|
+
- lib
|
|
162
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
163
|
+
requirements:
|
|
164
|
+
- - "~>"
|
|
165
|
+
- !ruby/object:Gem::Version
|
|
166
|
+
version: '2.2'
|
|
167
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
168
|
+
requirements:
|
|
169
|
+
- - ">"
|
|
170
|
+
- !ruby/object:Gem::Version
|
|
171
|
+
version: 1.3.1
|
|
172
|
+
requirements: []
|
|
173
|
+
rubyforge_project:
|
|
174
|
+
rubygems_version: 2.7.7
|
|
175
|
+
signing_key:
|
|
176
|
+
specification_version: 4
|
|
177
|
+
summary: An AST transformation framework.
|
|
178
|
+
test_files: []
|