rubyless 0.3.5 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ pkg
2
+ rdoc
3
+ *.gem
4
+ coverage
data/History.txt CHANGED
@@ -1,60 +1,75 @@
1
+ == 0.4.0 2010-03-21
2
+
3
+ * 4 major enhancement
4
+ * Parsing inheritance tree to get safe_method_type.
5
+ * Instance variable (ivar) support (declared as safe_methods).
6
+ * Added support for prepend variables to tranform methods like link("foo") to link(@node, "foo").
7
+ * Better handling of sub-types in signature matching.
8
+
9
+ * 5 minor enhancements
10
+ * Moved files into a ruby_less directory to enable file auto-loading.
11
+ * Raises RubyLess::NoMethodError / RubyLess::SyntaxError instead of generic Error.
12
+ * Improved error reporting use method signature.
13
+ * Added 'safe_literal_class' to enable/disable ruby literals.
14
+ * Added 'RubyLess.translate_string' method.
15
+
1
16
  == 0.3.5 2009-11-08
2
17
 
3
18
  * 1 major enhancement
4
- * added support for hash in signature: ['img', {'mode' => String, 'class' => String}]
19
+ * Added support for hash in signature: ['img', {'mode' => String, 'class' => String}].
5
20
 
6
21
  * 1 minor enhancement
7
- * added 'disable_safe_read' method
22
+ * Added 'disable_safe_read' method.
8
23
 
9
24
  == 0.3.4 2009-11-05
10
25
 
11
26
  * 1 minor enhancement
12
- * added 'safe_context' method
27
+ * Added 'safe_context' method.
13
28
 
14
29
  == 0.3.3 2009-10-26
15
30
 
16
31
  * 1 minor enhancement
17
- * added support for symbols (only used with helper)
32
+ * Added support for symbols (only used with helper).
18
33
 
19
34
  == 0.3.2 2009-10-15
20
35
 
21
36
  * 1 minor enhancement
22
- * removed 'ruby-debug' require
37
+ * Removed 'ruby-debug' require.
23
38
 
24
39
  == 0.3.1 2009-10-07
25
40
 
26
41
  * 3 major enhancements
27
- * method name in signatures should always be a string
28
- * type[:method] is always set and is always a string
29
- * fixed how class type is guessed from ActiveRecord column
42
+ * Method name in signatures should always be a string.
43
+ * Type[:method] is always set and is always a string.
44
+ * Fixed how class type is guessed from ActiveRecord column.
30
45
 
31
46
  * 1 minor enhancement
32
- * added 'safe_read' method to objects
47
+ * Added 'safe_read' method to objects.
33
48
 
34
49
  == 0.3.1 2009-10-03
35
50
 
36
51
  * 1 major enhancement
37
- * Moved from ParseTree to RubyParser
52
+ * Moved from ParseTree to RubyParser.
38
53
 
39
54
  * 1 minor enhancement:
40
- * Using Mr Bones to generate gems
55
+ * Using Mr Bones to generate gems.
41
56
 
42
57
  == 0.2.1 2009-07-01
43
58
 
44
59
  * 2 minor enhancements:
45
- * :[] method is now treated as in conventional ruby (foo[:bar], not foo.[](:bar))
46
- * Better error reporting in case of signature mismatch
60
+ * :[] method is now treated as in conventional ruby (foo[:bar], not foo.[](:bar)).
61
+ * Better error reporting in case of signature mismatch.
47
62
 
48
63
  == 0.2.0 2009-06-02
49
64
 
50
65
  * 1 major enhancement:
51
- * Added support for ActiveRecord attributes
66
+ * Added support for ActiveRecord attributes.
52
67
 
53
68
  * 2 minor enhancements:
54
69
  * Better documentation
55
- * Removed eval (this means safe methods are globally declared)
70
+ * Removed eval (this means safe methods are globally declared).
56
71
 
57
72
  == 0.1.0 2009-06-02
58
73
 
59
74
  * 1 major enhancement:
60
- * Initial alpha release
75
+ * Initial alpha release.
@@ -50,16 +50,16 @@ Or you can group all declarations in a single place with 'safe_method_for':
50
50
  You can now parse some ruby code:
51
51
 
52
52
  RubyLess.translate("!prev.ancestor?(main) && !node.ancestor?(main)", self)
53
- => "(not previous.ancestor?(@node) and not var1.ancestor?(@node))"
53
+ => "(not previous.ancestor?(@node) and not node.ancestor?(@node))"
54
54
 
55
55
  RubyLess.translate("id > 45 and (3 > -id or 3+3)", self)
56
- => "(var1.zip>45 and ((3>-var1.zip) or (3+3)))"
56
+ => "(node.zip>45 and ((3>-node.zip) or (3+3)))"
57
57
 
58
58
  RubyLess.translate("strftime(now, '%Y')", self)
59
59
  => "strftime(Time.now, \"%Y\")"
60
60
 
61
61
  RubyLess.translate("log_info(spouse, spouse.name)", self)
62
- => "(var1.spouse ? log_info(var1.spouse, var1.spouse.name) : nil)"
62
+ => "(node.spouse ? log_info(node.spouse, node.spouse.name) : nil)"
63
63
 
64
64
  You can look at the tests for an idea of how to declare things. If you have more questions, ask on zena's mailing list:
65
65
 
data/Rakefile CHANGED
@@ -1,48 +1,59 @@
1
- require "rubygems"
2
- require "rake/rdoctask"
1
+ require 'rubygems'
2
+ require 'rake'
3
3
 
4
- task :default => :test
4
+ require(File.join(File.dirname(__FILE__), 'lib/ruby_less/info'))
5
5
 
6
- require "rake/testtask"
7
- Rake::TestTask.new do |t|
8
- t.libs << "test"
9
- t.test_files = FileList["test/**/*_test.rb"]
10
- t.verbose = true
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.version = RubyLess::VERSION
10
+ gem.name = "rubyless"
11
+ gem.summary = %Q{RubyLess is an interpreter for "safe ruby"}
12
+ gem.description = %Q{RubyLess is an interpreter for "safe ruby". The idea is to transform some "unsafe" ruby code into safe, type checked ruby, eventually rewriting some variables or methods.}
13
+ gem.email = "gaspard@teti.ch"
14
+ gem.homepage = "http://zenadmin.org/546"
15
+ gem.authors = ["Gaspard Bucher"]
16
+ gem.add_dependency 'ruby_parser', '>= 2.0.4'
17
+ gem.add_dependency 'sexp_processor', '>= 3.0.1'
18
+ gem.add_development_dependency "shoulda", ">= 0"
19
+ gem.add_development_dependency "yamltest", ">= 0.5.3"
20
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
21
+ end
22
+ Jeweler::GemcutterTasks.new
23
+ rescue LoadError
24
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
11
25
  end
12
26
 
13
-
14
- # BONES gem management
27
+ require 'rake/testtask'
28
+ Rake::TestTask.new(:test) do |test|
29
+ test.libs << 'lib' << 'test'
30
+ test.pattern = 'test/**/*_test.rb'
31
+ test.verbose = true
32
+ end
15
33
 
16
34
  begin
17
- require 'bones'
18
- Bones.setup
35
+ require 'rcov/rcovtask'
36
+ Rcov::RcovTask.new do |test|
37
+ test.libs << 'test'
38
+ test.pattern = 'test/**/*_test.rb'
39
+ test.verbose = true
40
+ end
19
41
  rescue LoadError
20
- begin
21
- load 'tasks/setup.rb'
22
- rescue LoadError
23
- raise RuntimeError, '### please install the "bones" gem ###'
42
+ task :rcov do
43
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
24
44
  end
25
45
  end
26
46
 
27
- ensure_in_path 'lib'
28
- require 'rubyless'
29
-
30
- PROJ.name = 'rubyless'
31
- PROJ.authors = 'Gaspard Bucher'
32
- PROJ.email = 'gaspard@teti.ch'
33
- PROJ.url = 'http://zenadmin.org/546'
34
- PROJ.version = RubyLess::VERSION
35
- PROJ.rubyforge.name = 'rubyless'
36
-
37
- PROJ.spec.opts << '--color'
38
- PROJ.gem.files = (
39
- ['History.txt', 'README.txt', 'Rakefile', 'rubyless.gemspec'] +
40
- ['lib', 'test'].map do |d|
41
- Dir.glob("#{d}/**/*").reject {|path| File.basename(path) =~ /^\./ }
42
- end
43
- ).flatten
47
+ task :test => :check_dependencies
44
48
 
45
- PROJ.gem.dependencies << ['ruby_parser', '>= 2.0.4']
46
- PROJ.gem.dependencies << ['sexp_processor', '>= 3.0.1']
47
- PROJ.gem.development_dependencies << ['yamltest', '>= 0.5.3']
49
+ task :default => :test
48
50
 
51
+ require 'rake/rdoctask'
52
+ Rake::RDocTask.new do |rdoc|
53
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
54
+
55
+ rdoc.rdoc_dir = 'rdoc'
56
+ rdoc.title = "rubyless #{version}"
57
+ rdoc.rdoc_files.include('README*')
58
+ rdoc.rdoc_files.include('lib/**/*.rb')
59
+ end
@@ -0,0 +1,15 @@
1
+ require 'ruby_less/safe_class'
2
+
3
+
4
+ class Boolean
5
+ end
6
+
7
+ class Number
8
+ end
9
+
10
+ RubyLess::SafeClass.safe_literal_class Fixnum => Number, Float => Number, Symbol => Symbol, Regexp => Regexp
11
+ RubyLess::SafeClass.safe_method_for( Number,
12
+ [:==, Number] => Boolean, [:< , Number] => Boolean, [:> , Number] => Boolean,
13
+ [:<=, Number] => Boolean, [:>=, Number] => Boolean, [:- , Number] => Number,
14
+ [:+ , Number] => Number, [:* , Number] => Number, [:/ , Number] => Number,
15
+ [:% , Number] => Number, [:"-@"] => Number )
@@ -0,0 +1,4 @@
1
+ module RubyLess
2
+ class Error < Exception
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module RubyLess
2
+ VERSION = '0.4.0'
3
+ end
@@ -0,0 +1,47 @@
1
+ module RubyLess
2
+ class NoMethodError < RubyLess::Error
3
+ attr_reader :receiver, :klass, :signature
4
+
5
+ def initialize(receiver, klass, signature)
6
+ @receiver = receiver
7
+ @klass = klass
8
+ @signature = signature
9
+ end
10
+
11
+ def message
12
+ "#{receiver_with_class}: #{error_message} '#{method_with_arguments}'."
13
+ end
14
+
15
+ def error_message
16
+ if ivar?
17
+ "unknown instance variable"
18
+ else
19
+ "unknown method"
20
+ end
21
+ end
22
+
23
+ def receiver_with_class
24
+ @receiver ? "#{@receiver} (#{@klass})" : "(#{@klass.class})"
25
+ end
26
+
27
+ def method_with_arguments
28
+ method = @signature.first
29
+ signature = @signature[1..-1]
30
+ return method if ivar?
31
+ if signature.size == 0
32
+ arguments = ''
33
+ else
34
+ arguments = signature.map{|s| s.kind_of?(Class) ? s.to_s : s.inspect}.join(', ')
35
+ if signature.size == 1 && (signature.first.kind_of?(Array) || signature.first.kind_of?(Hash))
36
+ arguments = arguments[1..-2]
37
+ end
38
+ end
39
+ "#{method}(#{arguments})"
40
+ end
41
+
42
+ def ivar?
43
+ @signature.first =~ /\A@/
44
+ end
45
+
46
+ end
47
+ end
@@ -2,10 +2,6 @@ require 'rubygems'
2
2
  require 'ruby_parser'
3
3
  require 'sexp_processor'
4
4
 
5
- require 'basic_types'
6
- require 'typed_string'
7
- require 'safe_class'
8
-
9
5
  module RubyLess
10
6
  class RubyLessProcessor < SexpProcessor
11
7
  attr_reader :ruby
@@ -27,11 +23,18 @@ module RubyLess
27
23
  self.expected = TypedString
28
24
  end
29
25
 
30
- #def process(exp)
31
- # return nil if exp.nil?
32
- # method = exp.shift
33
- # send("process_#{method}", exp)
34
- #end
26
+ def process(exp)
27
+ super
28
+ rescue UnknownNodeError => err
29
+ if err.message =~ /Unknown node-type :(.*?) /
30
+ raise RubyLess::SyntaxError.new("'#{$1}' not available in RubyLess.")
31
+ else
32
+ raise RubyLess::SyntaxError.new(err.message)
33
+ end
34
+ # return nil if exp.nil?
35
+ # method = exp.shift
36
+ # send("process_#{method}", exp)
37
+ end
35
38
 
36
39
  def process_and(exp)
37
40
  t "(#{process(exp.shift)} and #{process(exp.shift)})", Boolean
@@ -51,9 +54,9 @@ module RubyLess
51
54
  false_res = process(exp.shift)
52
55
 
53
56
  if true_res && false_res && true_res.klass != false_res.klass
54
- raise "Error in conditional expression: '#{true_res}' and '#{false_res}' do not return results of same type (#{true_res.klass} != #{false_res.klass})."
57
+ raise RubyLess::SyntaxError.new("Error in conditional expression: '#{true_res}' and '#{false_res}' do not return results of same type (#{true_res.klass} != #{false_res.klass}).")
55
58
  end
56
- raise "Error in conditional expression." unless true_res || false_res
59
+ raise RubyLess::SyntaxError.new("Error in conditional expression.") unless true_res || false_res
57
60
  opts = {}
58
61
  opts[:nil] = true_res.nil? || true_res.could_be_nil? || false_res.nil? || false_res.could_be_nil?
59
62
  opts[:class] = true_res ? true_res.klass : false_res.klass
@@ -90,15 +93,18 @@ module RubyLess
90
93
  def process_vcall(exp)
91
94
  var_name = exp.shift
92
95
  unless opts = get_method([var_name], @helper, false)
93
- raise "Unknown variable or method '#{var_name}'."
96
+ raise RubyLess::NoMethodError.new("Unknown variable or method '#{var_name}'.")
94
97
  end
95
98
  method = opts[:method]
99
+ if args = opts[:prepend_args]
100
+ method = "#{method}(#{args.raw})"
101
+ end
96
102
  t method, opts
97
103
  end
98
104
 
99
105
  def process_lit(exp)
100
106
  lit = exp.shift
101
- t lit.inspect, lit.class == Symbol ? Symbol : Number
107
+ t lit.inspect, get_lit_class(lit.class)
102
108
  end
103
109
 
104
110
  def process_str(exp)
@@ -130,13 +136,17 @@ module RubyLess
130
136
  klass[key] = rhs.klass
131
137
  else
132
138
  # ERROR: invalid key
133
- raise "Invalid key type for hash (should be a literal value, was #{key.first.inspect})"
139
+ raise RubyLess::SyntaxError.new("Invalid key type for hash (should be a literal value, was #{key.first.inspect})")
134
140
  end
135
141
  end
136
142
 
137
143
  t "{#{result.join(', ')}}", :class => klass
138
144
  end
139
145
 
146
+ def process_ivar(exp)
147
+ method_call(nil, exp)
148
+ end
149
+
140
150
  private
141
151
  def t(content, opts = nil)
142
152
  if opts.nil?
@@ -174,14 +184,15 @@ module RubyLess
174
184
  if arg_sexp
175
185
  args = process(arg_sexp)
176
186
  if args == ''
187
+ args = nil
177
188
  signature = [method]
178
189
  else
179
190
  signature = [method] + [args.klass].flatten
180
191
  end
181
192
  # execution conditional
182
- cond = args.cond || []
193
+ cond = args ? (args.cond || []) : []
183
194
  else
184
- args = []
195
+ args = nil
185
196
  signature = [method]
186
197
  cond = []
187
198
  end
@@ -190,7 +201,7 @@ module RubyLess
190
201
  if receiver.could_be_nil?
191
202
  cond += receiver.cond
192
203
  end
193
- raise "'#{receiver}' does not respond to '#{method}(#{signature[1..-1].join(', ')})'." unless opts = get_method(signature, receiver.klass)
204
+ opts = get_method(receiver, signature)
194
205
  method = opts[:method]
195
206
  if method == '/'
196
207
  t_if cond, "(#{receiver.raw}#{method}#{args.raw} rescue nil)", opts.merge(:nil => true)
@@ -201,13 +212,15 @@ module RubyLess
201
212
  elsif method == '[]'
202
213
  t_if cond, "#{receiver.raw}[#{args.raw}]", opts
203
214
  else
204
- args = "(#{args.raw})" if args != ''
215
+ args = args_with_prepend(args, opts)
216
+ args = "(#{args.raw})" if args
205
217
  t_if cond, "#{receiver.raw}.#{method}#{args}", opts
206
218
  end
207
219
  else
208
- raise "Unknown method '#{method}(#{args.raw})'." unless opts = get_method(signature, @helper, false)
220
+ opts = get_method(nil, signature)
209
221
  method = opts[:method]
210
- args = "(#{args.raw})" if args != ''
222
+ args = args_with_prepend(args, opts)
223
+ args = "(#{args.raw})" if args
211
224
  t_if cond, "#{method}#{args}", opts
212
225
  end
213
226
  end
@@ -232,11 +245,37 @@ module RubyLess
232
245
  res
233
246
  end
234
247
 
235
- def get_method(signature, receiver, is_method = true)
236
- type = receiver.respond_to?(:safe_method_type) ? receiver.safe_method_type(signature) : SafeClass.safe_method_type_for(receiver, signature)
237
- return nil if !type || type[:class].kind_of?(Symbol) # we cannot send: no object.
248
+ def get_method(receiver, signature)
249
+ klass = receiver ? receiver.klass : @helper
250
+
251
+ type = klass.respond_to?(:safe_method_type) ? klass.safe_method_type(signature) : SafeClass.safe_method_type_for(klass, signature)
252
+
253
+ if type.nil?
254
+ # We try to match with the superclass of the arguments
255
+ end
256
+ raise RubyLess::NoMethodError.new(receiver, klass, signature) if !type || type[:class].kind_of?(Symbol) # we cannot send: no object.
238
257
 
239
258
  type[:class].kind_of?(Proc) ? type[:class].call(@helper, signature) : type
240
259
  end
260
+
261
+ def get_lit_class(klass)
262
+ unless lit_class = RubyLess::SafeClass.literal_class_for(klass)
263
+ raise RubyLess::SyntaxError.new("#{klass} literal not supported by RubyLess.")
264
+ end
265
+ lit_class
266
+ end
267
+
268
+ def args_with_prepend(args, opts)
269
+ if prepend_args = opts[:prepend_args]
270
+ if args
271
+ prepend_args.append_argument(args)
272
+ prepend_args
273
+ else
274
+ prepend_args
275
+ end
276
+ else
277
+ args
278
+ end
279
+ end
241
280
  end
242
281
  end
@@ -1,52 +1,51 @@
1
1
  module RubyLess
2
2
  module SafeClass
3
- @@_safe_methods ||= {} # defined for each class
4
- @@_safe_methods_all ||= {} # full list with inherited attributes
3
+ @@_safe_methods ||= {} # defined for each class
4
+ @@_safe_methods_parsed ||= {} # full list with inherited attributes
5
+ @@_safe_literal_classes ||= {}
5
6
 
6
7
  # List of safe methods for a specific class.
7
8
  def self.safe_methods_for(klass)
8
- @@_safe_methods_all[klass] ||= build_safe_methods_list(klass)
9
+ # Caching safe_methods_all is bad when modules are dynamically added / removed.
10
+ @@_safe_methods_parsed[klass] ||= build_safe_methods_list(klass)
9
11
  end
10
12
 
11
13
  # Return method type (options) if the given signature is a safe method for the class.
12
14
  def self.safe_method_type_for(klass, signature)
13
- if type = safe_methods_for(klass)[signature]
14
- type
15
- else
16
- # Signature might be ['name', {:mode => String, :type => Number}].
17
-
18
- # Replace all hashes in signature by Hash class and check for arguments
19
- signature_args = []
20
- signature = signature.map do |s|
21
- if s.kind_of?(Hash)
22
- signature_args << s
23
- Hash
24
- else
25
- signature_args << nil
26
- s
27
- end
28
- end
29
-
30
- if type = safe_methods_for(klass)[signature]
31
- unless allowed_args = type[:hash_args]
32
- # All arguments allowed
33
- return type
34
- end
15
+ # Signature might be ['name', {:mode => String, :type => Number}].
16
+ # build signature arguments
35
17
 
36
- # Verify arguments
37
- signature_args.each_with_index do |args, i|
38
- next unless args
39
- # verify for each position: ({:a => 3}, {:x => :y})
40
- return nil unless allowed_args_for_position = allowed_args[i]
41
- args.each do |k,v|
42
- return nil unless allowed_args_for_position[k] == v
43
- end
44
- end
45
- type
18
+ # Replace all hashes in signature by Hash class and check for arguments
19
+ signature_args = []
20
+ signature = signature.map do |s|
21
+ if s.kind_of?(Hash)
22
+ signature_args << s
23
+ Hash
46
24
  else
47
- nil
25
+ signature_args << nil
26
+ s
27
+ end
28
+ end
29
+ # Find safe method in all ancestry
30
+ klass.ancestors.each do |ancestor|
31
+ return nil if ancestor == RubyLess::SafeClass
32
+ if type = safe_method_with_hash_args(ancestor, signature, signature_args)
33
+ return type
48
34
  end
49
35
  end
36
+ nil
37
+ end
38
+
39
+ def self.literal_class_for(klass)
40
+ @@_safe_literal_classes[klass]
41
+ end
42
+
43
+ def self.safe_literal_class(hash)
44
+ @@_safe_literal_classes.merge!(hash)
45
+ end
46
+
47
+ def self.all_safe_methods
48
+ @@_safe_methods
50
49
  end
51
50
 
52
51
  # Declare a safe method for a given class ( same as #safe_method)
@@ -107,6 +106,10 @@ module RubyLess
107
106
  safe_method(methods_hash)
108
107
  end
109
108
 
109
+ def self.safe_literal_class(hash)
110
+ RubyLess::SafeClass.safe_literal_class(hash)
111
+ end
112
+
110
113
  # Declare a safe method to access a list of attributes.
111
114
  # This method should only be used when the class is linked with a database table and provides
112
115
  # proper introspection to detect types and the possibility of NULL values.
@@ -129,6 +132,30 @@ module RubyLess
129
132
  end
130
133
  end
131
134
 
135
+ # Declare a safe method to access a list of properties.
136
+ # This method should only be used in conjunction with the Property gem.
137
+ def self.safe_property(*properties)
138
+ columns = schema.columns
139
+ properties.each do |att|
140
+ if col = columns[att.to_s]
141
+ opts = {}
142
+ opts[:nil] = col.default.nil?
143
+ if col.number?
144
+ opts[:class] = Number
145
+ elsif col.text?
146
+ opts[:class] = String
147
+ else
148
+ opts[:class] = col.klass
149
+ end
150
+ opts[:method] = "prop['#{att.to_s.gsub("'",'')}']"
151
+ safe_method att.to_sym => opts
152
+ else
153
+ puts "Warning: could not declare safe_property '#{att}' (No property column with this name found in class #{self})"
154
+ end
155
+ end
156
+ end
157
+
158
+
132
159
  # Declare a safe method for a given class
133
160
  def self.safe_method_for(klass, signature)
134
161
  SafeClass.safe_method_for(klass, signature)
@@ -160,7 +187,7 @@ module RubyLess
160
187
 
161
188
  # Return the type if the given signature corresponds to a safe method for the object's class.
162
189
  def safe_method_type(signature)
163
- if type = self.class.safe_method_type(signature)
190
+ if type = SafeClass.safe_method_type_for(self.class, signature)
164
191
  type[:class].kind_of?(Symbol) ? self.send(type[:class], signature) : type
165
192
  end
166
193
  end
@@ -190,7 +217,7 @@ module RubyLess
190
217
  end
191
218
 
192
219
  def self.build_safe_methods_list(klass)
193
- list = klass.superclass.respond_to?(:safe_methods) ? klass.superclass.safe_methods : {}
220
+ list = SignatureHash.new
194
221
  (@@_safe_methods[klass] || {}).map do |signature, return_value|
195
222
  if return_value.kind_of?(Hash)
196
223
  return_value[:class] = parse_class(return_value[:class])
@@ -221,5 +248,27 @@ module RubyLess
221
248
  end
222
249
  end
223
250
  end
251
+
252
+ def self.safe_method_with_hash_args(klass, signature, hash_args)
253
+ if type = safe_methods_for(klass)[signature]
254
+ unless allowed_args = type[:hash_args]
255
+ # All arguments allowed
256
+ return type
257
+ end
258
+
259
+ # Verify arguments
260
+ hash_args.each_with_index do |args, i|
261
+ next unless args
262
+ # verify for each position: ({:a => 3}, {:x => :y})
263
+ return nil unless allowed_args_for_position = allowed_args[i]
264
+ args.each do |k,v|
265
+ return nil unless v.ancestors.include?(allowed_args_for_position[k])
266
+ end
267
+ end
268
+ type
269
+ else
270
+ nil
271
+ end
272
+ end
224
273
  end
225
274
  end
@@ -0,0 +1,33 @@
1
+
2
+ module RubyLess
3
+ class SignatureHash < Hash
4
+ alias get []
5
+
6
+ def [](signature)
7
+ if type = get(signature)
8
+ # fastest: all keys are equal
9
+ return type
10
+ elsif signature.kind_of?(Array)
11
+ size = signature.size
12
+ ancestors = signature.map {|k| k.kind_of?(Class) ? k.ancestors : [k]}
13
+
14
+ each do |key, type|
15
+ next unless key.size == size
16
+ ok = true
17
+ key.each_with_index do |k, i|
18
+ if !ancestors[i].include?(k)
19
+ ok = false
20
+ break
21
+ end
22
+ end
23
+ if ok
24
+ # insert in cache
25
+ self[signature] = type
26
+ return type
27
+ end
28
+ end
29
+ end
30
+ nil
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,4 @@
1
+ module RubyLess
2
+ class SyntaxError < RubyLess::Error
3
+ end
4
+ end