semdiff 0.1.0
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/.editorconfig +8 -0
- data/.rubocop.yml +28 -0
- data/.rubocop_todo.yml +41 -0
- data/.ruby-version +1 -0
- data/Gemfile +16 -0
- data/LICENSE +21 -0
- data/README.md +55 -0
- data/Rakefile +12 -0
- data/bin/semdiff +6 -0
- data/lib/semdiff/algebra_compiler.rb +180 -0
- data/lib/semdiff/aliasing_compiler.rb +65 -0
- data/lib/semdiff/cli/cli.rb +160 -0
- data/lib/semdiff/constants_compiler.rb +91 -0
- data/lib/semdiff/identity_compiler.rb +136 -0
- data/lib/semdiff/structures_compiler.rb +194 -0
- data/lib/semdiff/type_visitor.rb +207 -0
- data/lib/semdiff/utils/compiler_utils.rb +22 -0
- data/lib/semdiff/utils/io_utils.rb +238 -0
- data/lib/semdiff/version.rb +5 -0
- data/lib/semdiff.rb +31 -0
- data/sig/semdiff.rbs +4 -0
- metadata +152 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7bb06af96fb6fc68ab1d7af274dee105769b871eed5fcae927a5525a22ec5007
|
4
|
+
data.tar.gz: c32ed6ac3e2ceb10d2be082cec86d0313cc3cf4d0ed11214174f92e855227d0f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c26a50d430f514e80682b9b8b848010f56775d19cf81f47a0c661d344ec670b59781a092244683f8b9e0ba0d779d0869c4705d977108a452a504a91c188d5f47
|
7
|
+
data.tar.gz: a7ac9c34728f1805a2b5284ca20def2f85b8a531386ca96c766bc4d5483a6ec077250c8310a074bd2b6cce342eb04fc5625a5a813cb7cec73fcab74ec8425959
|
data/.editorconfig
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
inherit_from: .rubocop_todo.yml
|
2
|
+
|
3
|
+
Metrics/AbcSize:
|
4
|
+
Enabled: false
|
5
|
+
|
6
|
+
Metrics/CyclomaticComplexity:
|
7
|
+
Enabled: false
|
8
|
+
|
9
|
+
Metrics/PerceivedComplexity:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Metrics/BlockLength:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
Metrics/ClassLength:
|
16
|
+
Enabled: false
|
17
|
+
|
18
|
+
Metrics/MethodLength:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
Metrics/ModuleLength:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
AllCops:
|
25
|
+
NewCops: enable
|
26
|
+
Exclude:
|
27
|
+
- 'test/assets/yard/*'
|
28
|
+
- 'vendor/**/*'
|
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config --exclude-limit 30`
|
3
|
+
# on 2025-06-12 08:42:10 UTC using RuboCop version 1.76.1.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
8
|
+
|
9
|
+
# Offense count: 1
|
10
|
+
# This cop supports safe autocorrection (--autocorrect).
|
11
|
+
# Configuration parameters: Severity, Include.
|
12
|
+
# Include: **/*.gemspec
|
13
|
+
Gemspec/RequireMFA:
|
14
|
+
Exclude:
|
15
|
+
- 'semdiff.gemspec'
|
16
|
+
|
17
|
+
# Offense count: 2
|
18
|
+
# This cop supports safe autocorrection (--autocorrect).
|
19
|
+
# Configuration parameters: AutoCorrect, AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions.
|
20
|
+
# NotImplementedExceptions: NotImplementedError
|
21
|
+
Lint/UnusedMethodArgument:
|
22
|
+
Exclude:
|
23
|
+
- 'lib/semdiff/cli/cli.rb'
|
24
|
+
|
25
|
+
# Offense count: 4
|
26
|
+
# Configuration parameters: AllowedConstants.
|
27
|
+
Style/Documentation:
|
28
|
+
Exclude:
|
29
|
+
- 'spec/**/*'
|
30
|
+
- 'test/**/*'
|
31
|
+
- 'lib/semdiff.rb'
|
32
|
+
- 'lib/semdiff/cli/cli.rb'
|
33
|
+
- 'lib/semdiff/utils/compiler_utils.rb'
|
34
|
+
- 'lib/semdiff/utils/io_utils.rb'
|
35
|
+
|
36
|
+
# Offense count: 3
|
37
|
+
# This cop supports safe autocorrection (--autocorrect).
|
38
|
+
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
|
39
|
+
# URISchemes: http, https
|
40
|
+
Layout/LineLength:
|
41
|
+
Max: 136
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.1.2
|
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in semdiff.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
group :developement, :test do
|
9
|
+
gem 'rake', '~> 13.0'
|
10
|
+
|
11
|
+
gem 'minitest', '~> 5.0'
|
12
|
+
|
13
|
+
gem 'rubocop'
|
14
|
+
|
15
|
+
gem 'bundler-audit'
|
16
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Tesorion
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# Semdiff
|
2
|
+
|
3
|
+
Semantic differences for Ruby code changes.
|
4
|
+
|
5
|
+
Transforms Prism AST nodes to canonical forms whenever possible, for example: constant folding and reordering operands in expressions based on type signatures (YARD or RBS).
|
6
|
+
|
7
|
+
Uses GumTree/difftastic to generate the diff on canonical ASTs.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
12
|
+
|
13
|
+
Install the gem and add to the application's Gemfile by executing:
|
14
|
+
|
15
|
+
```bash
|
16
|
+
bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
17
|
+
```
|
18
|
+
|
19
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
Invoke with 2 target files and the `semdiff` executable, for example:
|
27
|
+
```
|
28
|
+
semdiff --yard --gumtree --diff-original test/assets/yard/before.rb test/assets/yard/after.rb
|
29
|
+
```
|
30
|
+
All options:
|
31
|
+
```
|
32
|
+
Usage: semdiff [options] BEFORE.rb AFTER.rb
|
33
|
+
|
34
|
+
Specific options:
|
35
|
+
-h, --help Prints this help
|
36
|
+
-o, --output-directory TARGET Directory to output normalized unparsed files (default temporary)
|
37
|
+
--ignore-comments Don't process and preserve comments in the unparsed files
|
38
|
+
--check-only Report whether there are any changes, but don't calculate them (much faster).
|
39
|
+
--diff-original Show a diff of the original files above the normalized files
|
40
|
+
--yard [TARGET] Use YARD (optional existing directory, default reparses input files)
|
41
|
+
--yard-files FILE1,FILE2,... Reparse specific YARD files (choose --yard or this, not both)
|
42
|
+
--rbs [TARGET] Use RBS signatures (default ./sig/)
|
43
|
+
--gumtree Use gumtree webdiff after default difftastic
|
44
|
+
--skip-difftastic Skip difftastic text diff
|
45
|
+
```
|
46
|
+
|
47
|
+
## Development
|
48
|
+
|
49
|
+
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.
|
50
|
+
|
51
|
+
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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
52
|
+
|
53
|
+
## Contributing
|
54
|
+
|
55
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/semdiff.
|
data/Rakefile
ADDED
data/bin/semdiff
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Semdiff
|
4
|
+
# AlgebraCompiler is a compiler that simplifies expressions with algebraic
|
5
|
+
# relations based on type information.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# ```ruby
|
9
|
+
# # Expression Canonical Form
|
10
|
+
# b + a a + b # commutativity
|
11
|
+
# c * a a * c # commutativity
|
12
|
+
# ( a + b ) + c a + b + c # associativity
|
13
|
+
# ( x * b ) + ( x * c ) x * ( b + c ) # distributivity
|
14
|
+
# x - ( - y ) x + y # negation
|
15
|
+
# ( x ** m ) * ( x ** n ) x **( m + n ) # exponentials
|
16
|
+
# ( a - b ) - b a - 2* b # optimization
|
17
|
+
# ```
|
18
|
+
class AlgebraCompiler < ::Prism::MutationCompiler
|
19
|
+
include Prism::DSL
|
20
|
+
include CompilerUtils
|
21
|
+
|
22
|
+
COMMUTATIVE_OPS = %i[+ *].freeze
|
23
|
+
ASSOCIATIVE_OPS = %i[+ *].freeze
|
24
|
+
|
25
|
+
def initialize(node_types)
|
26
|
+
@node_types = node_types
|
27
|
+
super()
|
28
|
+
end
|
29
|
+
|
30
|
+
def visit_call_node(node)
|
31
|
+
receiver = visit(node.receiver)
|
32
|
+
arguments = visit(node.arguments)
|
33
|
+
block = visit(node.block)
|
34
|
+
result = nil
|
35
|
+
|
36
|
+
if COMMUTATIVE_OPS.include?(node.name) &&
|
37
|
+
receiver &&
|
38
|
+
arguments&.arguments&.size == 1 &&
|
39
|
+
block.nil?
|
40
|
+
if ASSOCIATIVE_OPS.include?(node.name)
|
41
|
+
operands = flatten_associative_operation(node.name, receiver, arguments.arguments.first)
|
42
|
+
if operands.all? { |op| numeric?(op) }
|
43
|
+
sorted_operands = operands.sort_by { |op| sort_key(op) }
|
44
|
+
result = rebuild_associative_operation(node, node.name, sorted_operands)
|
45
|
+
end
|
46
|
+
else
|
47
|
+
lhs = receiver
|
48
|
+
rhs = arguments.arguments.first
|
49
|
+
if numeric?(lhs) && numeric?(rhs) && should_swap?(lhs, rhs)
|
50
|
+
result = node.copy(
|
51
|
+
receiver: rhs,
|
52
|
+
arguments: arguments_node(
|
53
|
+
node_id: arguments.node_id,
|
54
|
+
source: arguments.send(:source),
|
55
|
+
location: arguments.location,
|
56
|
+
flags: arguments.send(:flags),
|
57
|
+
arguments: [lhs]
|
58
|
+
)
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
if result
|
65
|
+
CompilerUtils.inherit_newline(node, result)
|
66
|
+
else
|
67
|
+
node.copy(receiver: receiver, arguments: arguments, block: block)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def visit_parentheses_node(node)
|
72
|
+
if node.multiple_statements?
|
73
|
+
statements = visit_all(node.body.body)
|
74
|
+
body = node.body.copy(body: statements)
|
75
|
+
node.copy(body: body)
|
76
|
+
else
|
77
|
+
inner = visit(node.body.body.first)
|
78
|
+
if inner.type == :call_node &&
|
79
|
+
ASSOCIATIVE_OPS.include?(inner.name) &&
|
80
|
+
inner.arguments&.arguments&.size == 1 &&
|
81
|
+
numeric?(inner.receiver) &&
|
82
|
+
numeric?(inner.arguments.arguments.first)
|
83
|
+
inner
|
84
|
+
else
|
85
|
+
body = node.body.copy(body: [inner])
|
86
|
+
node.copy(body: body)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def numeric?(node)
|
94
|
+
case node.type
|
95
|
+
when :integer_node, :float_node
|
96
|
+
true
|
97
|
+
else
|
98
|
+
types = @node_types[node.node_id]
|
99
|
+
types&.anybits?(NodeTypeFlags::NUMERIC)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def should_swap?(lhs, rhs)
|
104
|
+
sort_key(lhs) > sort_key(rhs)
|
105
|
+
end
|
106
|
+
|
107
|
+
def sort_key(node)
|
108
|
+
case node.type
|
109
|
+
when :local_variable_read_node, :instance_variable_read_node,
|
110
|
+
:class_variable_read_node, :global_variable_read_node,
|
111
|
+
:constant_read_node, :call_node
|
112
|
+
node.name.to_s
|
113
|
+
when :integer_node, :float_node
|
114
|
+
"_#{node.value}"
|
115
|
+
else
|
116
|
+
"~#{node.type}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def flatten_associative_operation(operation, left, right)
|
121
|
+
operands = []
|
122
|
+
|
123
|
+
left = unwrap_parentheses(left)
|
124
|
+
if left.type == :call_node &&
|
125
|
+
left.name == operation &&
|
126
|
+
left.arguments&.arguments&.size == 1 &&
|
127
|
+
left.block.nil?
|
128
|
+
operands.concat(flatten_associative_operation(operation, left.receiver, left.arguments.arguments.first))
|
129
|
+
else
|
130
|
+
operands << left
|
131
|
+
end
|
132
|
+
|
133
|
+
right = unwrap_parentheses(right)
|
134
|
+
if right.type == :call_node &&
|
135
|
+
right.name == operation &&
|
136
|
+
right.arguments&.arguments&.size == 1 &&
|
137
|
+
right.block.nil?
|
138
|
+
operands.concat(flatten_associative_operation(operation, right.receiver, right.arguments.arguments.first))
|
139
|
+
else
|
140
|
+
operands << right
|
141
|
+
end
|
142
|
+
|
143
|
+
operands
|
144
|
+
end
|
145
|
+
|
146
|
+
def unwrap_parentheses(node)
|
147
|
+
node = node.body.body.first while node.type == :parentheses_node && !node.multiple_statements?
|
148
|
+
node
|
149
|
+
end
|
150
|
+
|
151
|
+
def rebuild_associative_operation(original_node, operation, operands)
|
152
|
+
result = operands.first
|
153
|
+
|
154
|
+
operands[1..].each do |operand|
|
155
|
+
result = call_node(
|
156
|
+
node_id: original_node.node_id,
|
157
|
+
source: original_node.send(:source),
|
158
|
+
location: original_node.location,
|
159
|
+
flags: 0,
|
160
|
+
receiver: result,
|
161
|
+
call_operator_loc: nil,
|
162
|
+
name: operation,
|
163
|
+
message_loc: original_node.message_loc,
|
164
|
+
opening_loc: nil,
|
165
|
+
arguments: arguments_node(
|
166
|
+
node_id: original_node.arguments.node_id,
|
167
|
+
source: original_node.arguments.send(:source),
|
168
|
+
location: original_node.arguments.location,
|
169
|
+
flags: original_node.arguments.send(:flags),
|
170
|
+
arguments: [operand]
|
171
|
+
),
|
172
|
+
closing_loc: nil,
|
173
|
+
block: nil
|
174
|
+
)
|
175
|
+
end
|
176
|
+
|
177
|
+
result
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Semdiff
|
4
|
+
# AliasingCompiler is a compiler that unifies method aliases
|
5
|
+
# according to the Ruby style guide (https://rubystyle.guide/)
|
6
|
+
# based on type information.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# ```ruby
|
10
|
+
# # Original Canonical Form
|
11
|
+
# [].collect [].map
|
12
|
+
# {}.detect {}.find
|
13
|
+
# (1..5).find_all (1..5).select
|
14
|
+
# [1, 2].inject(:+) [1, 2].reduce(:+)
|
15
|
+
# [1].member?(1) [1].include?(1)
|
16
|
+
# "abc".length "abc".size
|
17
|
+
# [1,2,3].length [1,2,3].size
|
18
|
+
# ```
|
19
|
+
class AliasingCompiler < ::Prism::MutationCompiler
|
20
|
+
include Prism::DSL
|
21
|
+
include CompilerUtils
|
22
|
+
|
23
|
+
# NOTE: `Enumerables` (and others for `length`/`size`) typically
|
24
|
+
# respond to both the key and value method names in this map.
|
25
|
+
# We therefore assume that any Ruby object does (and should)
|
26
|
+
# have both methods for every pair, if they have at least one.
|
27
|
+
# This greatly simplifies the process of accounting for every
|
28
|
+
# (standard library) object and also allows us to map custom
|
29
|
+
# subclass (e.g., `Foo < Array`) methods automatically.
|
30
|
+
UNTYPED_ALIASES = {
|
31
|
+
collect: :map,
|
32
|
+
detect: :find,
|
33
|
+
find_all: :select,
|
34
|
+
inject: :reduce,
|
35
|
+
member?: :include?,
|
36
|
+
length: :size
|
37
|
+
}.freeze
|
38
|
+
|
39
|
+
def visit_call_node(node)
|
40
|
+
receiver = visit(node.receiver)
|
41
|
+
arguments = visit(node.arguments)
|
42
|
+
block = visit(node.block)
|
43
|
+
result = nil
|
44
|
+
|
45
|
+
if UNTYPED_ALIASES.key?(node.name)
|
46
|
+
canonical_name = UNTYPED_ALIASES[node.name]
|
47
|
+
# It might be worth updating the message_loc to reflect
|
48
|
+
# the new size. However, the underlying message buffer
|
49
|
+
# would still point to the original name.
|
50
|
+
result = node.copy(
|
51
|
+
name: canonical_name,
|
52
|
+
receiver: receiver,
|
53
|
+
arguments: arguments,
|
54
|
+
block: block
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
if result
|
59
|
+
CompilerUtils.inherit_newline(node, result)
|
60
|
+
else
|
61
|
+
node.copy(receiver: receiver, arguments: arguments, block: block)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Semdiff
|
4
|
+
class CLI
|
5
|
+
include IOUtils
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@options = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def parse(args)
|
12
|
+
require 'optionparser'
|
13
|
+
|
14
|
+
parser = OptionParser.new do |opts|
|
15
|
+
opts.banner = 'Usage: semdiff [options] BEFORE.rb AFTER.rb'
|
16
|
+
opts.separator ''
|
17
|
+
opts.separator 'Specific options:'
|
18
|
+
|
19
|
+
opts.on('-h', '--help', 'Prints this help') do
|
20
|
+
puts opts
|
21
|
+
exit 0
|
22
|
+
end
|
23
|
+
|
24
|
+
opts.on('-o TARGET', '--output-directory', 'Directory to output normalized unparsed files (default temporary)', String) do |dir|
|
25
|
+
@options[:output_directory] = dir
|
26
|
+
end
|
27
|
+
|
28
|
+
opts.on('--ignore-comments', "Don't process and preserve comments in the unparsed files") do |p|
|
29
|
+
@options[:ignore_comments] = p
|
30
|
+
end
|
31
|
+
|
32
|
+
opts.on('--check-only', "Report whether there are any changes, but don't calculate them (much faster).") do |c|
|
33
|
+
@options[:check_only] = c
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on('--diff-original', 'Show a diff of the original files above the normalized files') do |d|
|
37
|
+
@options[:diff_original] = d
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on('--yard [TARGET]', 'Use YARD (optional existing directory, default reparses input files)') do |target|
|
41
|
+
@options[:annotation_type] = :yard
|
42
|
+
@options[:annotation_target] = target
|
43
|
+
@options[:override] = true if target.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on('--yard-files FILE1,FILE2,...', 'Reparse specific YARD files (choose --yard or this, not both)', Array) do |files|
|
47
|
+
@options[:annotation_type] = :yard
|
48
|
+
@options[:annotation_target] = files
|
49
|
+
end
|
50
|
+
|
51
|
+
opts.on('--rbs [TARGET]', 'Use RBS signatures (default ./sig/)') do |target|
|
52
|
+
@options[:annotation_type] = :rbs
|
53
|
+
@options[:annotation_target] = target || 'sig'
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on('--gumtree', 'Use gumtree webdiff after default difftastic') do |g|
|
57
|
+
@options[:gumtree] = g
|
58
|
+
end
|
59
|
+
|
60
|
+
opts.on('--skip-difftastic', 'Skip difftastic text diff') do |d|
|
61
|
+
@options[:skip_difftastic] = d
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
begin
|
66
|
+
files = parser.parse!(args)
|
67
|
+
raise ArgumentError, "Expected 2 files but received #{files.size}" if files.size != 2
|
68
|
+
|
69
|
+
types = case @options[:annotation_type]
|
70
|
+
when :yard
|
71
|
+
::Typeguard::TypeModel::Builder.yard
|
72
|
+
@options[:annotation_target] ||= files
|
73
|
+
::Typeguard::TypeModel::Builder::IMPLEMENTATION.new(
|
74
|
+
@options[:annotation_target],
|
75
|
+
@options[:annotation_target].is_a?(Array)
|
76
|
+
).build
|
77
|
+
when :rbs
|
78
|
+
::Typeguard::TypeModel::Builder.rbs
|
79
|
+
::Typeguard::TypeModel::Builder::IMPLEMENTATION.new(
|
80
|
+
@options[:annotation_target],
|
81
|
+
false
|
82
|
+
).build
|
83
|
+
end
|
84
|
+
|
85
|
+
processed_asts = files.map do |file|
|
86
|
+
contents = File.read(file)
|
87
|
+
original_result = Prism.parse(contents)
|
88
|
+
ast = original_result.value
|
89
|
+
ast = ast.accept(Prism::DesugarCompiler.new)
|
90
|
+
ast = ast.accept(AliasingCompiler.new)
|
91
|
+
ast = ast.accept(StructuresCompiler.new)
|
92
|
+
node_types = TypeVisitor.new(types).visit(ast) unless types.nil?
|
93
|
+
ast = ast.accept(ConstantsCompiler.new)
|
94
|
+
ast = ast.accept(AlgebraCompiler.new(node_types)) unless types.nil?
|
95
|
+
ast = ast.accept(IdentityCompiler.new(node_types)) unless types.nil?
|
96
|
+
ast = ast.accept(ConstantsCompiler.new)
|
97
|
+
|
98
|
+
# NOTE: Uses the translation parser builder to
|
99
|
+
# translate the existing AST to whitequark/parser,
|
100
|
+
# so that we can rewrite with unparser and feed the
|
101
|
+
# files to difftastic/GumTree.
|
102
|
+
translator = Prism::Translation::Parser.new(
|
103
|
+
parser: Struct.new(:prism_ast, :original_result) do
|
104
|
+
def parse(source, **options)
|
105
|
+
Struct.new(:value, :comments, :magic_comments, :data_loc, :errors, :warnings, :source).new(
|
106
|
+
prism_ast,
|
107
|
+
original_result.comments,
|
108
|
+
original_result.magic_comments,
|
109
|
+
original_result.data_loc,
|
110
|
+
original_result.errors,
|
111
|
+
original_result.warnings,
|
112
|
+
original_result.source
|
113
|
+
)
|
114
|
+
end
|
115
|
+
end.new(ast, original_result)
|
116
|
+
)
|
117
|
+
source_buffer = Parser::Source::Buffer.new(file)
|
118
|
+
source_buffer.source = contents
|
119
|
+
translator.send(@options[:ignore_comments] ? 'parse' : 'parse_with_comments', source_buffer)
|
120
|
+
end
|
121
|
+
|
122
|
+
unparsed = processed_asts.map do |r|
|
123
|
+
r.is_a?(Array) ? Unparser.unparse(r.first, comments: r.last) : Unparser.unparse(r)
|
124
|
+
end
|
125
|
+
file_names = files.map { |f| File.basename(f) }
|
126
|
+
if !@options[:skip_difftastic] && @options[:diff_original]
|
127
|
+
system <<~CMD
|
128
|
+
difft #{files.first} #{files.last} \
|
129
|
+
#{'--check-only --exit-code' if @options[:check_only]} \
|
130
|
+
#{'--ignore-comments' if @options[:ignore_comments]}
|
131
|
+
CMD
|
132
|
+
end
|
133
|
+
return if @options[:skip_difftastic] && !@options[:gumtree]
|
134
|
+
|
135
|
+
with_diff_files(unparsed, file_names, output_directory: @options[:output_directory]) do |b, a|
|
136
|
+
unless @options[:skip_difftastic]
|
137
|
+
system <<~CMD
|
138
|
+
difft #{b.path} #{a.path} \
|
139
|
+
#{'--check-only --exit-code' if @options[:check_only]} \
|
140
|
+
#{'--ignore-comments' if @options[:ignore_comments]}
|
141
|
+
CMD
|
142
|
+
end
|
143
|
+
if @options[:gumtree]
|
144
|
+
system <<~CMD
|
145
|
+
gumtree webdiff #{b.path} #{a.path} \
|
146
|
+
-g ruby-treesitter-ng
|
147
|
+
CMD
|
148
|
+
end
|
149
|
+
end
|
150
|
+
rescue OptionParser::InvalidOption => e
|
151
|
+
puts e.message
|
152
|
+
puts parser
|
153
|
+
puts "Invalid argument (use semdiff --help): #{e}"
|
154
|
+
exit 1
|
155
|
+
end
|
156
|
+
|
157
|
+
@options
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|