furnace 0.3.0.beta2 → 0.3.0.beta3
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.
- data/.yardopts +1 -1
- data/Rakefile +9 -0
- data/furnace.gemspec +2 -0
- data/lib/furnace.rb +12 -0
- data/lib/furnace/ast.rb +2 -1
- data/lib/furnace/ast/node.rb +19 -1
- data/lib/furnace/ast/processor.rb +238 -1
- data/lib/furnace/ast/sexp.rb +30 -0
- data/lib/furnace/version.rb +1 -1
- data/test/ast_test.rb +17 -3
- metadata +37 -3
data/.yardopts
CHANGED
@@ -1 +1 @@
|
|
1
|
-
-m markdown --protected
|
1
|
+
-r {Furnace} -m markdown --protected
|
data/Rakefile
CHANGED
@@ -9,4 +9,13 @@ task :test do
|
|
9
9
|
Dir["test/**/*_test.rb"].each do |file|
|
10
10
|
load file
|
11
11
|
end
|
12
|
+
end
|
13
|
+
|
14
|
+
PAGES_REPO = 'git@github.com:whitequark/furnace'
|
15
|
+
|
16
|
+
task :pages do
|
17
|
+
system "git clone #{PAGES_REPO} gh-temp/ -b gh-pages; rm gh-temp/* -rf" or abort
|
18
|
+
system "yardoc -o gh-temp/; cd gh-temp/; git add -A; git commit -m 'Updated pages.'" or abort
|
19
|
+
system "cd gh-temp/; git push -f origin gh-pages" or abort
|
20
|
+
FileUtils.rm_rf 'gh-temp'
|
12
21
|
end
|
data/furnace.gemspec
CHANGED
data/lib/furnace.rb
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
# Furnace is a set of tools for writing compilers, translators or
|
2
|
+
# static analyzers--any programs which read, manipulate or transform
|
3
|
+
# other programs.
|
4
|
+
#
|
5
|
+
# Currently it provides three independent modules, grouped by the main
|
6
|
+
# data structure being used:
|
7
|
+
#
|
8
|
+
# * Abstract syntax trees: {AST}
|
9
|
+
# * Control flow graphs: {CFG}
|
10
|
+
# * Transformations: {Transform}
|
11
|
+
#
|
12
|
+
# Additionally, a simple {Graphviz} adapter is provided.
|
1
13
|
module Furnace
|
2
14
|
require "furnace/version"
|
3
15
|
|
data/lib/furnace/ast.rb
CHANGED
@@ -8,11 +8,12 @@ module Furnace
|
|
8
8
|
# garbage collector, but completely eliminates all concurrency
|
9
9
|
# and aliasing problems.
|
10
10
|
#
|
11
|
-
# See also {Node} and {
|
11
|
+
# See also {Node}, {Processor} and {Sexp} for additional
|
12
12
|
# recommendations and design patterns.
|
13
13
|
module AST
|
14
14
|
end
|
15
15
|
|
16
16
|
require_relative "ast/node"
|
17
17
|
require_relative "ast/processor"
|
18
|
+
require_relative "ast/sexp"
|
18
19
|
end
|
data/lib/furnace/ast/node.rb
CHANGED
@@ -67,10 +67,14 @@ module Furnace::AST
|
|
67
67
|
# attribute readers for such variables. The values passed in the hash
|
68
68
|
# are not frozen or whitelisted; such behavior can also be implemented\
|
69
69
|
# by subclassing Node and overriding this method.
|
70
|
+
#
|
71
|
+
# @return [nil]
|
70
72
|
def assign_properties(properties)
|
71
73
|
properties.each do |name, value|
|
72
74
|
instance_variable_set :"@#{name}", value
|
73
75
|
end
|
76
|
+
|
77
|
+
nil
|
74
78
|
end
|
75
79
|
protected :assign_properties
|
76
80
|
|
@@ -84,6 +88,11 @@ module Furnace::AST
|
|
84
88
|
# yield `(foo)`.
|
85
89
|
#
|
86
90
|
# If the resulting node would be identical to `self`, does nothing.
|
91
|
+
#
|
92
|
+
# @param [Symbol, nil] type
|
93
|
+
# @param [Array, nil] children
|
94
|
+
# @param [Hash, nil] properties
|
95
|
+
# @return [AST::Node]
|
87
96
|
def updated(type=nil, children=nil, properties=nil)
|
88
97
|
new_type = type || @type
|
89
98
|
new_children = children || @children
|
@@ -100,6 +109,8 @@ module Furnace::AST
|
|
100
109
|
|
101
110
|
# Compares `self` to `other`, possibly converting with `to_ast`. Only
|
102
111
|
# `type` and `children` are compared; metadata is deliberately ignored.
|
112
|
+
#
|
113
|
+
# @return [Boolean]
|
103
114
|
def ==(other)
|
104
115
|
if equal?(other)
|
105
116
|
true
|
@@ -113,11 +124,16 @@ module Furnace::AST
|
|
113
124
|
end
|
114
125
|
|
115
126
|
# Converts `self` to a concise s-expression, omitting any children.
|
127
|
+
#
|
128
|
+
# @return [String]
|
116
129
|
def to_s
|
117
130
|
"(#{fancy_type} ...)"
|
118
131
|
end
|
119
132
|
|
120
133
|
# Converts `self` to a pretty-printed s-expression.
|
134
|
+
#
|
135
|
+
# @param [Integer] indent Base indentation level.
|
136
|
+
# @return [String]
|
121
137
|
def to_sexp(indent=0)
|
122
138
|
sexp = "#{" " * indent}(#{fancy_type}"
|
123
139
|
|
@@ -139,7 +155,7 @@ module Furnace::AST
|
|
139
155
|
end
|
140
156
|
alias :inspect :to_sexp
|
141
157
|
|
142
|
-
#
|
158
|
+
# @return [AST::Node] self
|
143
159
|
def to_ast
|
144
160
|
self
|
145
161
|
end
|
@@ -149,6 +165,8 @@ module Furnace::AST
|
|
149
165
|
# Returns `@type` with all underscores replaced by dashes. This allows
|
150
166
|
# to write symbol literals without quotes in Ruby sources and yet have
|
151
167
|
# nicely looking s-expressions.
|
168
|
+
#
|
169
|
+
# @return [String]
|
152
170
|
def fancy_type
|
153
171
|
@type.to_s.gsub('_', '-')
|
154
172
|
end
|
@@ -1,5 +1,234 @@
|
|
1
1
|
module Furnace::AST
|
2
|
-
|
2
|
+
# Processor is a class which helps transforming one AST into another.
|
3
|
+
# In a nutshell, the {#process} method accepts a {Node} and dispatches
|
4
|
+
# it to a handler corresponding to its type, and returns a (possibly)
|
5
|
+
# updated variant of the node.
|
6
|
+
#
|
7
|
+
# Processor has a set of associated design patterns. They are best
|
8
|
+
# explained with a concrete example. Let's define a simple arithmetic
|
9
|
+
# language and an AST format for it:
|
10
|
+
#
|
11
|
+
# Terminals (AST nodes which do not have other AST nodes inside):
|
12
|
+
#
|
13
|
+
# * `(integer <int-literal>)`,
|
14
|
+
#
|
15
|
+
# Nonterminals (AST nodes with other nodes as children):
|
16
|
+
#
|
17
|
+
# * `(add <node> <node>)`,
|
18
|
+
# * `(multiply <node> <node>)`,
|
19
|
+
# * `(divide <node> <node>)`,
|
20
|
+
# * `(negate <node>)`,
|
21
|
+
# * `(store <node> <string-literal>)`: stores value of `<node>` into a variable named `<string-literal>`,
|
22
|
+
# * `(load <string-literal>)`: loads value of a variable named `<string-literal>`,
|
23
|
+
# * `(each <node> ...): computes each of the `<node>`s and prints the result.
|
24
|
+
#
|
25
|
+
# Furnace AST nodes all have the same Ruby class, and therefore they don't
|
26
|
+
# know how to traverse themselves. (A solution which dynamically checks the
|
27
|
+
# type of children is possible, but is slow and error-prone.) So, a subclass
|
28
|
+
# of Processor which knows how to traverse the entire tree should be defined.
|
29
|
+
# Such subclass has a handler for each nonterminal node which recursively
|
30
|
+
# processes children nodes:
|
31
|
+
#
|
32
|
+
# require 'furnace'
|
33
|
+
# include Furnace
|
34
|
+
#
|
35
|
+
# class ArithmeticsProcessor < AST::Processor
|
36
|
+
# # This method traverses any binary operators such as (add) or (multiply).
|
37
|
+
# def process_binary_op(node)
|
38
|
+
# # Children aren't decomposed automatically; it is suggested to use Ruby
|
39
|
+
# # multiple assignment expansion, as it is very convenient here.
|
40
|
+
# left_expr, right_expr = node.children
|
41
|
+
#
|
42
|
+
# # AST::Node#updated won't change node type if nil is passed as a first
|
43
|
+
# # argument, which allows to reuse the same handler for multiple node types
|
44
|
+
# # using `alias' (below).
|
45
|
+
# node.updated(nil, [
|
46
|
+
# process(left_expr),
|
47
|
+
# process(right_expr)
|
48
|
+
# ])
|
49
|
+
# end
|
50
|
+
# alias on_add process_binary_op
|
51
|
+
# alias on_multiply process_binary_op
|
52
|
+
# alias on_divide process_binary_op
|
53
|
+
#
|
54
|
+
# def on_negate(node)
|
55
|
+
# # It is also possible to use #process_all for more compact code
|
56
|
+
# # if every child is a Node.
|
57
|
+
# node.updated(nil, process_all(node.children))
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# def on_store(node)
|
61
|
+
# expr, variable_name = node.children
|
62
|
+
#
|
63
|
+
# # Note that variable_name is not a Node and thus isn't passed to #process.
|
64
|
+
# node.updated(nil, [
|
65
|
+
# process(expr),
|
66
|
+
# variable_name
|
67
|
+
# ])
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# # (load) is effectively a terminal node, and so it does not need
|
71
|
+
# # an explicit handler, as the following is the default behavior.
|
72
|
+
# def on_load(node)
|
73
|
+
# nil
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# def on_each(node)
|
77
|
+
# node.updated(nil, process_all(node.children))
|
78
|
+
# end
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# Let's test our ArithmeticsProcessor:
|
82
|
+
#
|
83
|
+
# include AST::Sexp
|
84
|
+
# expr = s(:add, s(:integer, 2), s(:integer, 2))
|
85
|
+
#
|
86
|
+
# p ArithmeticsProcessor.new.process(expr) == expr # => true
|
87
|
+
#
|
88
|
+
# As expected, it does not change anything at all. This isn't actually
|
89
|
+
# very useful, so let's now define a Calculator, which will compute the
|
90
|
+
# expression values:
|
91
|
+
#
|
92
|
+
# # This Processor folds nonterminal nodes and returns an (integer)
|
93
|
+
# # terminal node.
|
94
|
+
# class ArithmeticsCalculator < ArithmeticsProcessor
|
95
|
+
# def compute_op(node)
|
96
|
+
# # First, node children are processed and then unpacked to local
|
97
|
+
# # variables.
|
98
|
+
# nodes = process_all(node.children)
|
99
|
+
#
|
100
|
+
# if nodes.all? { |node| node.type == :integer }
|
101
|
+
# # If each of those nodes represents a literal, we can fold this
|
102
|
+
# # node!
|
103
|
+
# values = nodes.map { |node| node.children.first }
|
104
|
+
# AST::Node.new(:integer, [
|
105
|
+
# yield(values)
|
106
|
+
# ])
|
107
|
+
# else
|
108
|
+
# # Otherwise, we can just leave the current node in the tree and
|
109
|
+
# # only update it with processed children nodes, which can be
|
110
|
+
# # partially folded.
|
111
|
+
# node.updated(nil, nodes)
|
112
|
+
# end
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# def on_add(node)
|
116
|
+
# compute_op(node) { |left, right| left + right }
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# def on_multiply(node)
|
120
|
+
# compute_op(node) { |left, right| left * right }
|
121
|
+
# end
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
# Let's check:
|
125
|
+
#
|
126
|
+
# p ArithmeticsCalculator.new.process(expr) # => (integer 4)
|
127
|
+
#
|
128
|
+
# Excellent, the calculator works! Now, a careful reader could notice that
|
129
|
+
# the ArithmeticsCalculator does not know how to divide numbers. What if we
|
130
|
+
# pass an expression with division to it?
|
131
|
+
#
|
132
|
+
# expr_with_division = \
|
133
|
+
# s(:add,
|
134
|
+
# s(:integer, 1),
|
135
|
+
# s(:divide,
|
136
|
+
# s(:add, s(:integer, 8), s(:integer, 4)),
|
137
|
+
# s(:integer, 3))) # 1 + (8 + 4) / 3
|
138
|
+
#
|
139
|
+
# folded_expr_with_division = ArithmeticsCalculator.new.process(expr_with_division)
|
140
|
+
# p folded_expr_with_division
|
141
|
+
# # => (add
|
142
|
+
# # (integer 1)
|
143
|
+
# # (divide
|
144
|
+
# # (integer 12)
|
145
|
+
# # (integer 3)))
|
146
|
+
#
|
147
|
+
# As you can see, the expression was folded _partially_: the inner `(add)` node which
|
148
|
+
# could be computed was folded to `(integer 12)`, the `(divide)` node is left as-is
|
149
|
+
# because there is no computing handler for it, and the root `(add)` node was also left
|
150
|
+
# as it is because some of its children were not literals.
|
151
|
+
#
|
152
|
+
# Note that this partial folding is only possible because the _data_ format, i.e.
|
153
|
+
# the format in which the computed values of the nodes are represented, is the same as
|
154
|
+
# the AST itself.
|
155
|
+
#
|
156
|
+
# Let's extend our ArithmeticsCalculator class further.
|
157
|
+
#
|
158
|
+
# class ArithmeticsCalculator
|
159
|
+
# def on_divide(node)
|
160
|
+
# compute_op(node) { |left, right| left / right }
|
161
|
+
# end
|
162
|
+
#
|
163
|
+
# def on_negate(node)
|
164
|
+
# # Note how #compute_op works regardless of the operator arity.
|
165
|
+
# compute_op(node) { |value| -value }
|
166
|
+
# end
|
167
|
+
# end
|
168
|
+
#
|
169
|
+
# Now, let's apply our renewed ArithmeticsCalculator to a partial result of previous
|
170
|
+
# evaluation:
|
171
|
+
#
|
172
|
+
# p ArithmeticsCalculator.new.process(expr_with_division) # => (integer 5)
|
173
|
+
#
|
174
|
+
# Five! Excellent. This is also pretty much how CRuby 1.8 executed its programs.
|
175
|
+
#
|
176
|
+
# Now, let's do some automated bug searching. Division by zero is an error, right?
|
177
|
+
# So if we could detect that someone has divided by zero before the program is even
|
178
|
+
# run, that could save some debugging time.
|
179
|
+
#
|
180
|
+
# class DivisionByZeroVerifier < ArithmeticsProcessor
|
181
|
+
# class VerificationFailure < Exception; end
|
182
|
+
#
|
183
|
+
# def on_divide(node)
|
184
|
+
# # You need to process the children to handle nested divisions
|
185
|
+
# # such as:
|
186
|
+
# # (divide
|
187
|
+
# # (integer 1)
|
188
|
+
# # (divide (integer 1) (integer 0))
|
189
|
+
# left, right = process_all(node.children)
|
190
|
+
#
|
191
|
+
# if right.type == :integer &&
|
192
|
+
# right.children.first == 0
|
193
|
+
# raise VerificationFailure, "Ouch! This code divides by zero."
|
194
|
+
# end
|
195
|
+
# end
|
196
|
+
#
|
197
|
+
# def divides_by_zero?(ast)
|
198
|
+
# process(ast)
|
199
|
+
# false
|
200
|
+
# rescue VerificationFailure
|
201
|
+
# true
|
202
|
+
# end
|
203
|
+
# end
|
204
|
+
#
|
205
|
+
# nice_expr = \
|
206
|
+
# s(:divide,
|
207
|
+
# s(:add, s(:integer, 10), s(:integer, 2)),
|
208
|
+
# s(:integer, 4))
|
209
|
+
#
|
210
|
+
# p DivisionByZeroVerifier.new.divides_by_zero?(nice_expr)
|
211
|
+
# # => false. Good.
|
212
|
+
#
|
213
|
+
# bad_expr = \
|
214
|
+
# s(:add, s(:integer, 10),
|
215
|
+
# s(:divide, s(:integer, 1), s(:integer, 0)))
|
216
|
+
#
|
217
|
+
# p DivisionByZeroVerifier.new.divides_by_zero?(bad_expr)
|
218
|
+
# # => true. WHOOPS. DO NOT RUN THIS.
|
219
|
+
#
|
220
|
+
# Of course, this won't detect more complex cases... unless you use some partial
|
221
|
+
# evaluation before! The possibilites are endless. Have fun.
|
222
|
+
class Processor
|
223
|
+
# Dispatches `node`. If a node has type `:foo`, then a handler named
|
224
|
+
# `on_foo` is invoked with one argument, the `node`; if there isn't
|
225
|
+
# such a handler, {#handler_missing} is invoked with the same argument.
|
226
|
+
#
|
227
|
+
# If the handler returns `nil`, `node` is returned; otherwise, the return
|
228
|
+
# value of the handler is passed along.
|
229
|
+
#
|
230
|
+
# @param [AST::Node, nil] node
|
231
|
+
# @return [AST::Node]
|
3
232
|
def process(node)
|
4
233
|
if node
|
5
234
|
# Invoke a specific handler
|
@@ -16,12 +245,20 @@ module Furnace::AST
|
|
16
245
|
node
|
17
246
|
end
|
18
247
|
|
248
|
+
# {#process}es each node from `nodes` and returns an array of results.
|
249
|
+
#
|
250
|
+
# @param [Array<AST::Node>] nodes
|
251
|
+
# @return [Array<AST::Node>]
|
19
252
|
def process_all(nodes)
|
20
253
|
nodes.map do |node|
|
21
254
|
process node
|
22
255
|
end
|
23
256
|
end
|
24
257
|
|
258
|
+
# Default handler. Does nothing.
|
259
|
+
#
|
260
|
+
# @param [AST::Node] node
|
261
|
+
# @return [AST::Node, nil]
|
25
262
|
def handler_missing(node)
|
26
263
|
end
|
27
264
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Furnace::AST
|
2
|
+
# This simple module is very useful in the cases where one needs
|
3
|
+
# to define deeply nested ASTs from Ruby code, for example, in
|
4
|
+
# tests. It should be used like this:
|
5
|
+
#
|
6
|
+
# describe YourLanguage::AST do
|
7
|
+
# include Sexp
|
8
|
+
#
|
9
|
+
# it "should correctly parse expressions" do
|
10
|
+
# YourLanguage.parse("1 + 2 * 3").should ==
|
11
|
+
# s(:add,
|
12
|
+
# s(:integer, 1),
|
13
|
+
# s(:multiply,
|
14
|
+
# s(:integer, 2),
|
15
|
+
# s(:integer, 3)))
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# This way the amount of boilerplate code is greatly reduced.
|
20
|
+
module Sexp
|
21
|
+
# Creates a {Node} with type `type` and children `children`.
|
22
|
+
# Note that the resulting node is of the type AST::Node and not a
|
23
|
+
# subclass.
|
24
|
+
# This would not pose a problem with comparisons, as {Node#==}
|
25
|
+
# ignores metadata.
|
26
|
+
def s(type, *children)
|
27
|
+
Node.new(type, children)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/furnace/version.rb
CHANGED
data/test/ast_test.rb
CHANGED
@@ -94,9 +94,7 @@ describe AST::Processor do
|
|
94
94
|
lambda { |ast| ast.to_sexp == text }
|
95
95
|
end
|
96
96
|
|
97
|
-
class MockProcessor
|
98
|
-
include AST::Processor
|
99
|
-
|
97
|
+
class MockProcessor < AST::Processor
|
100
98
|
attr_reader :counts
|
101
99
|
|
102
100
|
def initialize
|
@@ -163,4 +161,20 @@ describe AST::Processor do
|
|
163
161
|
| (invoke :func))
|
164
162
|
SEXP
|
165
163
|
end
|
164
|
+
|
165
|
+
extend Furnace::AST::Sexp
|
166
|
+
|
167
|
+
it 'should build sexps' do
|
168
|
+
s(:add,
|
169
|
+
s(:integer, 1),
|
170
|
+
s(:multiply,
|
171
|
+
s(:integer, 2),
|
172
|
+
s(:integer, 3))).should have_sexp(<<-SEXP)
|
173
|
+
|(add
|
174
|
+
| (integer 1)
|
175
|
+
| (multiply
|
176
|
+
| (integer 2)
|
177
|
+
| (integer 3)))
|
178
|
+
SEXP
|
179
|
+
end
|
166
180
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: furnace
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.0.
|
4
|
+
version: 0.3.0.beta3
|
5
5
|
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-11-
|
12
|
+
date: 2012-11-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -43,6 +43,38 @@ dependencies:
|
|
43
43
|
- - ~>
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: '1.1'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: yard
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: redcarpet
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
46
78
|
description: Furnace is a static code analysis framework for dynamic languages, aimed
|
47
79
|
at efficient type and behavior inference.
|
48
80
|
email:
|
@@ -61,6 +93,7 @@ files:
|
|
61
93
|
- lib/furnace/ast.rb
|
62
94
|
- lib/furnace/ast/node.rb
|
63
95
|
- lib/furnace/ast/processor.rb
|
96
|
+
- lib/furnace/ast/sexp.rb
|
64
97
|
- lib/furnace/cfg.rb
|
65
98
|
- lib/furnace/cfg/algorithms.rb
|
66
99
|
- lib/furnace/cfg/graph.rb
|
@@ -85,7 +118,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
85
118
|
version: '0'
|
86
119
|
segments:
|
87
120
|
- 0
|
88
|
-
hash:
|
121
|
+
hash: 4298627248929422388
|
89
122
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
123
|
none: false
|
91
124
|
requirements:
|
@@ -99,3 +132,4 @@ signing_key:
|
|
99
132
|
specification_version: 3
|
100
133
|
summary: A static code analysis framework
|
101
134
|
test_files: []
|
135
|
+
has_rdoc:
|