exhale 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/.ruby-version +1 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +149 -0
- data/Rakefile +96 -0
- data/exe/exhale +6 -0
- data/exhale.gemspec +43 -0
- data/lib/exhale/cli.rb +148 -0
- data/lib/exhale/contract/markdown.rb +107 -0
- data/lib/exhale/contract/reference.rb +63 -0
- data/lib/exhale/contract/resolver.rb +142 -0
- data/lib/exhale/contract.rb +194 -0
- data/lib/exhale/dry/check.rb +266 -0
- data/lib/exhale/dry/fingerprints.rb +105 -0
- data/lib/exhale/dry/gate.rb +425 -0
- data/lib/exhale/dry/index.rb +59 -0
- data/lib/exhale/dry/matcher.rb +468 -0
- data/lib/exhale/dry/normalizer/erb.rb +289 -0
- data/lib/exhale/dry/normalizer/ruby.rb +193 -0
- data/lib/exhale/dry/normalizer.rb +26 -0
- data/lib/exhale/errors.rb +27 -0
- data/lib/exhale/git.rb +279 -0
- data/lib/exhale/report.rb +203 -0
- data/lib/exhale/shape.rb +32 -0
- data/lib/exhale/source_files.rb +84 -0
- data/lib/exhale/unit.rb +27 -0
- data/lib/exhale/units/erb.rb +30 -0
- data/lib/exhale/units/ruby.rb +283 -0
- data/lib/exhale/version.rb +5 -0
- data/lib/exhale.rb +25 -0
- metadata +152 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "../errors"
|
|
5
|
+
require_relative "../unit"
|
|
6
|
+
|
|
7
|
+
module Exhale
|
|
8
|
+
module Units
|
|
9
|
+
# Finds the units in one Ruby file: methods, and the bodies of Rails DSL
|
|
10
|
+
# calls (scopes, callbacks, validations, rescue_from, job hooks).
|
|
11
|
+
module Ruby
|
|
12
|
+
DSL = %i[
|
|
13
|
+
scope default_scope validate validates_each
|
|
14
|
+
before_validation after_validation
|
|
15
|
+
before_save around_save after_save before_create around_create after_create
|
|
16
|
+
before_update around_update after_update before_destroy around_destroy after_destroy
|
|
17
|
+
after_commit after_create_commit after_update_commit after_destroy_commit after_save_commit after_rollback
|
|
18
|
+
after_initialize after_find after_touch
|
|
19
|
+
before_action after_action around_action prepend_before_action append_before_action
|
|
20
|
+
prepend_after_action append_after_action rescue_from
|
|
21
|
+
before_perform after_perform around_perform before_enqueue after_enqueue around_enqueue
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
# Concern blocks whose contents read as the class body itself.
|
|
25
|
+
CLASS_BODIES = %i[included prepended concerning].freeze
|
|
26
|
+
|
|
27
|
+
# Blocks that reopen their constant receiver as a class body.
|
|
28
|
+
CLASS_EVALS = %i[class_eval class_exec module_eval module_exec].freeze
|
|
29
|
+
|
|
30
|
+
# Block-defined methods, and whether each defines a singleton method.
|
|
31
|
+
DEFINE_METHODS = { define_method: false, define_singleton_method: true }.freeze
|
|
32
|
+
|
|
33
|
+
# A string naming a constant, as `rescue_from "Billing::Declined"` takes.
|
|
34
|
+
CONSTANT_PATH = /\A(?:::)?[A-Z]\w*(?:::[A-Z]\w*)*\z/
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
def extract(source, path)
|
|
39
|
+
result = Prism.parse(source)
|
|
40
|
+
if result.failure?
|
|
41
|
+
error = result.errors.first
|
|
42
|
+
raise ParseError.new(path, error.location.start_line, error.message)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Walker.new(path).walk(result.value)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Walks a file keeping the namespace a statement sits in, and whether
|
|
49
|
+
# it sits in a singleton body (`class << self`, `class_methods do`).
|
|
50
|
+
class Walker
|
|
51
|
+
def initialize(path)
|
|
52
|
+
@path = path
|
|
53
|
+
@units = []
|
|
54
|
+
@ordinals = Hash.new(0)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def walk(program)
|
|
58
|
+
visit(program, "Object", false)
|
|
59
|
+
number_repeats
|
|
60
|
+
@units
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def visit(node, namespace, singleton)
|
|
66
|
+
case node
|
|
67
|
+
when Prism::ModuleNode, Prism::ClassNode
|
|
68
|
+
visit_body(node.body, nest(namespace, node.constant_path), false)
|
|
69
|
+
when Prism::SingletonClassNode then singleton_class(node, namespace)
|
|
70
|
+
when Prism::DefNode then add_method(node, namespace, singleton)
|
|
71
|
+
when Prism::ConstantWriteNode, Prism::ConstantPathWriteNode then constant_write(node, namespace, singleton)
|
|
72
|
+
when Prism::CallNode then call(node, namespace, singleton)
|
|
73
|
+
else visit_children(node, namespace, singleton)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def visit_body(body, namespace, singleton)
|
|
78
|
+
visit(body, namespace, singleton) if body
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def visit_children(node, namespace, singleton)
|
|
82
|
+
node.compact_child_nodes.each { |child| visit(child, namespace, singleton) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# `class << self` opens this namespace's singleton class, `class <<
|
|
86
|
+
# Other` opens Other's. Any other object's isn't a namespace.
|
|
87
|
+
def singleton_class(node, namespace)
|
|
88
|
+
owner = owner_of(node.expression, namespace)
|
|
89
|
+
visit_body(node.body, owner, true) if owner
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# A def nested inside this one belongs to it, so the walk stops here.
|
|
93
|
+
# `def obj.x` on a non-constant object belongs to no namespace.
|
|
94
|
+
def add_method(node, namespace, singleton)
|
|
95
|
+
return add(:method, node, namespace, node.name.to_s, singleton ? "." : "#") unless node.receiver
|
|
96
|
+
|
|
97
|
+
owner = owner_of(node.receiver, namespace)
|
|
98
|
+
add(:method, node, owner, node.name.to_s, ".") if owner
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# `Point = Struct.new(:x) do ... end` and `Class.new do ... end`
|
|
102
|
+
# define their methods on the constant.
|
|
103
|
+
def constant_write(node, namespace, singleton)
|
|
104
|
+
value = node.value
|
|
105
|
+
if value.is_a?(Prism::CallNode) && value.block.is_a?(Prism::BlockNode)
|
|
106
|
+
name = node.is_a?(Prism::ConstantWriteNode) ? join(namespace, node.name.to_s) : nest(namespace, node.target)
|
|
107
|
+
visit_body(value.block.body, name, false)
|
|
108
|
+
else
|
|
109
|
+
visit_children(node, namespace, singleton)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def call(node, namespace, singleton)
|
|
114
|
+
block = node.block if node.block.is_a?(Prism::BlockNode)
|
|
115
|
+
if block && DEFINE_METHODS.key?(node.name) && (node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode))
|
|
116
|
+
return define_method(node, block, namespace, singleton || DEFINE_METHODS[node.name])
|
|
117
|
+
end
|
|
118
|
+
return receiver_call(node, block, namespace, singleton) if node.receiver
|
|
119
|
+
|
|
120
|
+
if CLASS_BODIES.include?(node.name) && block then visit_body(block.body, namespace, false)
|
|
121
|
+
elsif node.name == :class_methods && block then visit_body(block.body, namespace, true)
|
|
122
|
+
elsif DSL.include?(node.name) && (body = block || lambda_argument(node)) then add_dsl(node, body, namespace)
|
|
123
|
+
else visit_children(node, namespace, singleton)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# `Order.class_eval do ... end` reads as Order's class body.
|
|
128
|
+
def receiver_call(node, block, namespace, singleton)
|
|
129
|
+
if block && CLASS_EVALS.include?(node.name) && constant?(node.receiver)
|
|
130
|
+
visit_body(block.body, resolve(namespace, node.receiver), false)
|
|
131
|
+
else
|
|
132
|
+
visit_children(node, namespace, singleton)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def define_method(node, block, namespace, singleton)
|
|
137
|
+
separator = singleton ? "." : "#"
|
|
138
|
+
name = literal_name(node) || "#{node.name}[#{next_ordinal(namespace, node.name)}]"
|
|
139
|
+
add(:method, block, namespace, name, separator)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def add_dsl(node, body, namespace)
|
|
143
|
+
add(:dsl, body, namespace, dsl_name(node, namespace), ".")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# A body named by its first argument, a symbol or string (`scope
|
|
147
|
+
# :settled`, `scope "settled"`) or constants (`rescue_from
|
|
148
|
+
# Billing::Declined`), keeps its identity as the bodies around it
|
|
149
|
+
# change. An unnamed body gets an ordinal, and ordinals drift: a new
|
|
150
|
+
# `before_save do` above it renumbers it.
|
|
151
|
+
def dsl_name(node, namespace)
|
|
152
|
+
constants = Array(node.arguments&.arguments).take_while { |argument| constant_reference?(argument) }
|
|
153
|
+
return "#{node.name}(#{constants.map { |c| constant_text(c) }.join(", ")})" if constants.any?
|
|
154
|
+
|
|
155
|
+
name = literal_name(node)
|
|
156
|
+
return "#{node.name}(:#{name})" if name
|
|
157
|
+
|
|
158
|
+
"#{node.name}[#{next_ordinal(namespace, node.name)}]"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def constant_reference?(node)
|
|
162
|
+
constant?(node) || (node.is_a?(Prism::StringNode) && node.unescaped.match?(CONSTANT_PATH))
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def constant_text(node)
|
|
166
|
+
node.is_a?(Prism::StringNode) ? node.unescaped.delete_prefix("::") : constant_name(node).last
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# `scope :x, -> { ... }`, or the older `scope :x, lambda { ... }`.
|
|
170
|
+
def lambda_argument(node)
|
|
171
|
+
Array(node.arguments&.arguments).each do |argument|
|
|
172
|
+
return argument if argument.is_a?(Prism::LambdaNode)
|
|
173
|
+
return argument.block if lambda_call?(argument)
|
|
174
|
+
end
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def lambda_call?(node)
|
|
179
|
+
node.is_a?(Prism::CallNode) && node.receiver.nil? && %i[lambda proc].include?(node.name) &&
|
|
180
|
+
node.block.is_a?(Prism::BlockNode)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def literal_name(node)
|
|
184
|
+
first = node.arguments&.arguments&.first
|
|
185
|
+
first.unescaped if first.is_a?(Prism::SymbolNode) || first.is_a?(Prism::StringNode)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def next_ordinal(namespace, macro)
|
|
189
|
+
@ordinals[[namespace, macro]] += 1
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def add(kind, node, namespace, name, separator)
|
|
193
|
+
@units << Unit.new(kind: kind, identity: "#{namespace}#{separator}#{name}", namespace: namespace,
|
|
194
|
+
name: name, path: @path, start_line: node.location.start_line,
|
|
195
|
+
end_line: last_line(node), language: :ruby, node: node)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# A name defined twice in one file (in both branches of an `if`, or in
|
|
199
|
+
# a class reopened further down) still gets one identity per unit, in
|
|
200
|
+
# source order: `Cart#total`, then `Cart#total[2]`.
|
|
201
|
+
def number_repeats
|
|
202
|
+
seen = Hash.new(0)
|
|
203
|
+
@units.each do |unit|
|
|
204
|
+
count = seen[unit.identity] += 1
|
|
205
|
+
next if count == 1
|
|
206
|
+
|
|
207
|
+
unit.identity = "#{unit.identity}[#{count}]"
|
|
208
|
+
unit.name = "#{unit.name}[#{count}]"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# A heredoc's body and terminator sit below the line its node ends on.
|
|
213
|
+
def last_line(node)
|
|
214
|
+
last = node.location.end_line
|
|
215
|
+
stack = [node]
|
|
216
|
+
until stack.empty?
|
|
217
|
+
current = stack.pop
|
|
218
|
+
last = [last, current.closing_loc.start_line].max if heredoc?(current)
|
|
219
|
+
stack.concat(current.compact_child_nodes)
|
|
220
|
+
end
|
|
221
|
+
last
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def heredoc?(node)
|
|
225
|
+
node.respond_to?(:heredoc?) && node.heredoc? && node.closing_loc
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# The namespace a `self` or constant receiver names; nil for any
|
|
229
|
+
# other object.
|
|
230
|
+
def owner_of(node, namespace)
|
|
231
|
+
case node
|
|
232
|
+
when Prism::SelfNode then namespace
|
|
233
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then resolve(namespace, node)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def constant?(node)
|
|
238
|
+
node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# A definition (`class Foo`) nests inside the enclosing namespace.
|
|
242
|
+
def nest(namespace, constant_path)
|
|
243
|
+
anchor, name = constant_name(constant_path)
|
|
244
|
+
anchor == :root ? name : join(namespace, name)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# A reference (`def Foo.x`, `class << Foo`, `Foo.class_eval`) names
|
|
248
|
+
# an existing constant. Short of full constant lookup, it resolves to
|
|
249
|
+
# an enclosing namespace when its first segment names one, so `def
|
|
250
|
+
# Billing.x` inside `module Billing` is `Billing.x`, and otherwise
|
|
251
|
+
# reads as written.
|
|
252
|
+
def resolve(namespace, node)
|
|
253
|
+
anchor, name = constant_name(node)
|
|
254
|
+
return name if anchor == :root
|
|
255
|
+
return join(namespace, name) if anchor == :self
|
|
256
|
+
|
|
257
|
+
segments = namespace.split("::")
|
|
258
|
+
index = segments.rindex(name.split("::").first)
|
|
259
|
+
index ? (segments[0...index] + [name]).join("::") : name
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def join(namespace, name)
|
|
263
|
+
namespace == "Object" ? name : "#{namespace}::#{name}"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# [anchor, "Foo::Bar"]. anchor is :root for a leading `::`, :self for
|
|
267
|
+
# a leading `self::` (the current namespace), and nil otherwise.
|
|
268
|
+
def constant_name(node)
|
|
269
|
+
case node
|
|
270
|
+
when Prism::ConstantReadNode then [nil, node.name.to_s]
|
|
271
|
+
when Prism::ConstantPathNode
|
|
272
|
+
return [:root, node.name.to_s] if node.parent.nil?
|
|
273
|
+
return [:self, node.name.to_s] if node.parent.is_a?(Prism::SelfNode)
|
|
274
|
+
|
|
275
|
+
anchor, parent = constant_name(node.parent)
|
|
276
|
+
[anchor, "#{parent}::#{node.name}"]
|
|
277
|
+
else [nil, node.slice]
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
data/lib/exhale.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "exhale/version"
|
|
4
|
+
require_relative "exhale/errors"
|
|
5
|
+
require_relative "exhale/shape"
|
|
6
|
+
require_relative "exhale/unit"
|
|
7
|
+
require_relative "exhale/git"
|
|
8
|
+
require_relative "exhale/source_files"
|
|
9
|
+
require_relative "exhale/units/ruby"
|
|
10
|
+
require_relative "exhale/units/erb"
|
|
11
|
+
require_relative "exhale/contract"
|
|
12
|
+
require_relative "exhale/dry/normalizer/ruby"
|
|
13
|
+
require_relative "exhale/dry/normalizer/erb"
|
|
14
|
+
require_relative "exhale/dry/normalizer"
|
|
15
|
+
require_relative "exhale/dry/fingerprints"
|
|
16
|
+
require_relative "exhale/dry/index"
|
|
17
|
+
require_relative "exhale/dry/matcher"
|
|
18
|
+
require_relative "exhale/dry/gate"
|
|
19
|
+
require_relative "exhale/dry/check"
|
|
20
|
+
require_relative "exhale/report"
|
|
21
|
+
require_relative "exhale/cli"
|
|
22
|
+
|
|
23
|
+
# exhale gates the exhale of every pull request. See README.md.
|
|
24
|
+
module Exhale
|
|
25
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: exhale
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Obie Fernandez
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-03 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: bigdecimal
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: herb
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.11'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.11'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: prism
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.9'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.9'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: minitest
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '6.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '6.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rake
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '13.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '13.0'
|
|
83
|
+
description: exhale gates the exhale of every pull request in a Rails app. Its first
|
|
84
|
+
check, exhale dry, sweeps the whole codebase for duplicated Ruby and ERB, scores
|
|
85
|
+
near-copies by rarity-weighted structural similarity, and fails the build while
|
|
86
|
+
any copy is undeclared. Deliberate duplication is declared in the Contract, next
|
|
87
|
+
to the reason for it. Built on Prism and Herb.
|
|
88
|
+
email:
|
|
89
|
+
- obiefernandez@gmail.com
|
|
90
|
+
executables:
|
|
91
|
+
- exhale
|
|
92
|
+
extensions: []
|
|
93
|
+
extra_rdoc_files: []
|
|
94
|
+
files:
|
|
95
|
+
- ".ruby-version"
|
|
96
|
+
- CHANGELOG.md
|
|
97
|
+
- LICENSE.txt
|
|
98
|
+
- README.md
|
|
99
|
+
- Rakefile
|
|
100
|
+
- exe/exhale
|
|
101
|
+
- exhale.gemspec
|
|
102
|
+
- lib/exhale.rb
|
|
103
|
+
- lib/exhale/cli.rb
|
|
104
|
+
- lib/exhale/contract.rb
|
|
105
|
+
- lib/exhale/contract/markdown.rb
|
|
106
|
+
- lib/exhale/contract/reference.rb
|
|
107
|
+
- lib/exhale/contract/resolver.rb
|
|
108
|
+
- lib/exhale/dry/check.rb
|
|
109
|
+
- lib/exhale/dry/fingerprints.rb
|
|
110
|
+
- lib/exhale/dry/gate.rb
|
|
111
|
+
- lib/exhale/dry/index.rb
|
|
112
|
+
- lib/exhale/dry/matcher.rb
|
|
113
|
+
- lib/exhale/dry/normalizer.rb
|
|
114
|
+
- lib/exhale/dry/normalizer/erb.rb
|
|
115
|
+
- lib/exhale/dry/normalizer/ruby.rb
|
|
116
|
+
- lib/exhale/errors.rb
|
|
117
|
+
- lib/exhale/git.rb
|
|
118
|
+
- lib/exhale/report.rb
|
|
119
|
+
- lib/exhale/shape.rb
|
|
120
|
+
- lib/exhale/source_files.rb
|
|
121
|
+
- lib/exhale/unit.rb
|
|
122
|
+
- lib/exhale/units/erb.rb
|
|
123
|
+
- lib/exhale/units/ruby.rb
|
|
124
|
+
- lib/exhale/version.rb
|
|
125
|
+
homepage: https://github.com/tools4imps/exhale-ruby
|
|
126
|
+
licenses:
|
|
127
|
+
- MIT
|
|
128
|
+
metadata:
|
|
129
|
+
source_code_uri: https://github.com/tools4imps/exhale-ruby
|
|
130
|
+
changelog_uri: https://github.com/tools4imps/exhale-ruby/blob/main/CHANGELOG.md
|
|
131
|
+
rubygems_mfa_required: 'true'
|
|
132
|
+
post_install_message:
|
|
133
|
+
rdoc_options: []
|
|
134
|
+
require_paths:
|
|
135
|
+
- lib
|
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
137
|
+
requirements:
|
|
138
|
+
- - ">="
|
|
139
|
+
- !ruby/object:Gem::Version
|
|
140
|
+
version: '3.3'
|
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0'
|
|
146
|
+
requirements: []
|
|
147
|
+
rubygems_version: 3.5.11
|
|
148
|
+
signing_key:
|
|
149
|
+
specification_version: 4
|
|
150
|
+
summary: 'The contraction gate for Rails: no PR merges while the codebase holds duplication
|
|
151
|
+
the Contract doesn''t keep'
|
|
152
|
+
test_files: []
|