rubyless 0.3.5 → 0.4.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.
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