diff --git a/automation/include/aegisub/argcheck.moon b/automation/include/aegisub/argcheck.moon new file mode 100644 index 000000000..b7492cdf7 --- /dev/null +++ b/automation/include/aegisub/argcheck.moon @@ -0,0 +1,78 @@ +-- Copyright (c) 2014, Thomas Goyne +-- +-- Permission to use, copy, modify, and distribute this software for any +-- purpose with or without fee is hereby granted, provided that the above +-- copyright notice and this permission notice appear in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +-- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +-- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +-- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- +-- Aegisub Project http://www.aegisub.org/ + +assert = assert +error = error +select = select +tostring = tostring +type = type + +is_type = (v, ty, expected) -> + ty == expected or (ty == 'table' and v.__class and v.__class.__name == expected) + +(argfmt) -> + assert type(argfmt) == 'string' + min_args = 0 + max_args = 0 + checks = {} + for arg in argfmt\gmatch '[^ ]+' + if arg == '...' + max_args = nil + break + + max_args += 1 + + optional = arg\sub(1, 1) == '?' + if optional + arg = arg\sub 2 + else + min_args += 1 + + if arg\find '|' + types = [ty for ty in arg\gmatch '[^|]+'] + checks[max_args] = (i, v) -> + if v == nil + return if optional + error "Argument ##{i} should be a #{arg}, is nil", 4 + ty = type v + for argtype in *types + return if is_type v, ty, argtype + error "Argument ##{i} should be a #{arg}, is #{ty} (#{v})", 3 + else + checks[max_args] = (i, v) -> + if v == nil + return if optional + error "Argument ##{i} should be a #{arg}, is nil", 4 + ty = type v + return if is_type v, ty, arg + error "Argument ##{i} should be a #{arg}, is #{ty} (#{v})", 3 + + (fn) -> (...) -> + arg_count = select '#', ... + if arg_count < min_args or (max_args and arg_count > max_args) + if min_args == max_args + error "Expected #{min_args} arguments, got #{arg_count}", 3 + else if max_args + error "Expected #{min_args}-#{max_args} arguments, got #{arg_count}", 3 + else + error "Expected at least #{min_args} arguments, got #{arg_count}", 3 + + for i=1,arg_count + if not checks[i] then break + checks[i] i, select i, ... + + fn ... + diff --git a/automation/include/aegisub/lfs.moon b/automation/include/aegisub/lfs.moon index 989467a6d..850df2f7a 100644 --- a/automation/include/aegisub/lfs.moon +++ b/automation/include/aegisub/lfs.moon @@ -15,6 +15,8 @@ -- Aegisub Project http://www.aegisub.org/ impl = require 'aegisub.__lfs_impl' + +check = require 'aegisub.argcheck' ffi = require 'ffi' ffi_util = require 'aegisub.ffi' @@ -29,7 +31,7 @@ number_ret = (f) -> (...) -> res, err = f ... tonumber(res), err -attributes = (path, field) -> +attributes = check'string ?string' (path, field) -> switch field when 'mode' res, err = impl.get_mode path @@ -62,7 +64,7 @@ class dir_iter if err then error err, 2 ffi_util.string str -dir = (path) -> +dir = check'string' (path) -> obj, err = impl.dir_new path if err error 2, err @@ -71,10 +73,10 @@ dir = (path) -> return { :attributes - chdir: number_ret impl.chdir - currentdir: string_ret impl.currentdir + chdir: check'string' number_ret impl.chdir + currentdir: check'' string_ret impl.currentdir :dir - mkdir: number_ret impl.mkdir - rmdir: number_ret impl.rmdir - touch: number_ret impl.touch + mkdir: check'string' number_ret impl.mkdir + rmdir: check'string'number_ret impl.rmdir + touch: check'string'number_ret impl.touch } diff --git a/automation/include/aegisub/re.moon b/automation/include/aegisub/re.moon index 1b278a16e..c487c3424 100644 --- a/automation/include/aegisub/re.moon +++ b/automation/include/aegisub/re.moon @@ -20,6 +20,7 @@ type = type bit = require 'bit' ffi = require 'ffi' ffi_util = require 'aegisub.ffi' +check = require 'aegisub.argcheck' ffi.cdef[[ typedef struct agi_re_flag { @@ -91,12 +92,6 @@ unpack_args = (...) -> return 0, ... unless flags_start process_flags(select flags_start, ...), select_first flags_start - 1, ... --- Typecheck a variable and throw an error if it fails -check_arg = (arg, expected_type, argn, func_name, level) -> - if type(arg) != expected_type - error "Argument #{argn} to #{func_name} should be a '#{expected_type}', is '#{type(arg)}' (#{arg})", - level + 1 - -- Replace a match with the value returned from func when passed the match replace_match = (match, func, str, last, acc) -> -- Copy everything between the last match and this match @@ -155,9 +150,7 @@ class RegEx new: (@_regex, @_level) => - gsplit: (str, skip_empty, max_split) => - @_check_self! - check_arg str, 'string', 2, 'gsplit', @_level + gsplit: check'RegEx string ?boolean ?number' (str, skip_empty, max_split) => if not max_split or max_split <= 0 then max_split = str\len() start = 0 @@ -187,15 +180,10 @@ class RegEx do_split - split: (str, skip_empty, max_split) => - @_check_self! - check_arg str, 'string', 2, 'split', @_level + split: check'RegEx string ?boolean ?number' (str, skip_empty, max_split) => [v for v in @gsplit str, skip_empty, max_split] - gfind: (str) => - @_check_self! - check_arg str, 'string', 2, 'gfind', @_level - + gfind: check'RegEx string' (str) => start = 0 -> first, last = search(@_regex, str, start) @@ -204,31 +192,19 @@ class RegEx start = if last > start then last else start + 1 str\sub(first, last), first, last - find: (str) => - @_check_self! - check_arg str, 'string', 2, 'find', @_level - + find: check'RegEx string' (str) => ret = [str: s, first: f, last: l for s, f, l in @gfind(str)] next(ret) and ret - sub: (str, repl, max_count) => - @_check_self! - check_arg str, 'string', 2, 'sub', @_level - if max_count != nil - check_arg max_count, 'number', 4, 'sub', @_level - + sub: check'RegEx string string|function ?number' (str, repl, max_count) => max_count = str\len() + 1 if not max_count or max_count == 0 if type(repl) == 'function' do_replace_fun @, repl, str, max_count elseif type(repl) == 'string' replace @_regex, repl, str, max_count - else - error "Argument 2 to sub should be a string or function, is '#{type(repl)}' (#{repl})", @_level - gmatch: (str, start) => - @_check_self! - check_arg str, 'string', 2, 'gmatch', @_level + gmatch: check'RegEx string ?number' (str, start) => start = if start then start - 1 else 0 m = match @_regex, str, start @@ -245,10 +221,7 @@ class RegEx last: last + start } - match: (str, start) => - @_check_self! - check_arg(str, 'string', 2, 'match', @_level) - + match: check'RegEx string ?number' (str, start) => ret = [v for v in @gmatch str, start] -- Return nil rather than a empty table so that if re.match(...) works return nil if next(ret) == nil @@ -271,16 +244,13 @@ invoke = (str, pattern, fn, flags, ...) -> compiled_regex[fn](compiled_regex, str, ...) -- Generate a static version of a method with arg type checking -gen_wrapper = (impl_name) -> (str, pattern, ...) -> - check_arg str, 'string', 1, impl_name, 2 - check_arg pattern, 'string', 2, impl_name, 2 +gen_wrapper = (impl_name) -> check'string string ...' (str, pattern, ...) -> invoke str, pattern, impl_name, unpack_args ... -- And now at last the actual public API do re = { - compile: (pattern, ...) -> - check_arg pattern, 'string', 1, 'compile', 2 + compile: check'string ...' (pattern, ...) -> real_compile pattern, 2, process_flags(...), 2 split: gen_wrapper 'split' diff --git a/automation/include/aegisub/unicode.moon b/automation/include/aegisub/unicode.moon index e66090485..3b8444e98 100644 --- a/automation/include/aegisub/unicode.moon +++ b/automation/include/aegisub/unicode.moon @@ -29,11 +29,13 @@ -- http://www.ietf.org/rfc/rfc2279.txt impl = require 'aegisub.__unicode_impl' + +check = require 'aegisub.argcheck' ffi = require 'ffi' ffi_util = require 'aegisub.ffi' err_buff = ffi.new 'char *[1]' -conv_func = (f) -> (str) -> +conv_func = (f) -> check'string' (str) -> err_buff[0] = nil result = f str, err_buff errmsg = ffi_util.string err_buff[0] @@ -44,7 +46,7 @@ conv_func = (f) -> (str) -> local unicode unicode = -- Return the number of bytes occupied by the character starting at the i'th byte in s - charwidth: (s, i) -> + charwidth: check'string ?number' (s, i) -> b = s\byte i or 1 -- FIXME, something in karaskel results in this case, shouldn't happen -- What would "proper" behaviour be? Zero? Or just explode? @@ -55,7 +57,7 @@ unicode = else 4 -- Returns an iterator function for iterating over the characters in s - chars: (s) -> + chars: check'string' (s) -> curchar, i = 0, 1 -> return if i > s\len() @@ -67,13 +69,13 @@ unicode = -- Returns the number of characters in s -- Runs in O(s:len()) time! - len: (s) -> + len: check'string' (s) -> n = 0 n += 1 for c in unicode.chars s n -- Get codepoint of first char in s - codepoint: (s) -> + codepoint: check'string' (s) -> -- Basic case, ASCII b = s\byte 1 return b if b < 128 diff --git a/automation/include/aegisub/util.moon b/automation/include/aegisub/util.moon index b9ff61ae3..42a642ee6 100644 --- a/automation/include/aegisub/util.moon +++ b/automation/include/aegisub/util.moon @@ -20,14 +20,16 @@ sformat = string.format tonumber = tonumber type = type +check = require 'aegisub.argcheck' + local * -- Make a shallow copy of a table -copy = (tbl) -> {k, v for k, v in pairs tbl} +copy = check'table' (tbl) -> {k, v for k, v in pairs tbl} -- Make a deep copy of a table -- Retains equality of table references inside the copy and handles self-referencing structures -deep_copy = (tbl) -> +deep_copy = check'table' (tbl) -> seen = {} copy = (val) -> return val if type(val) != 'table' @@ -44,7 +46,7 @@ ass_alpha = (a) -> sformat "&H%02X&", a ass_style_color = (r, g, b, a) -> sformat "&H%02X%02X%02X%02X", a, b, g, r -- Extract colour components of an ASS colour -extract_color = (s) -> +extract_color = check'string' (s) -> local a, b, g, r -- Try a style first @@ -68,10 +70,10 @@ extract_color = (s) -> return tonumber(r, 16), tonumber(g, 16) or 0, tonumber(b, 16) or 0, tonumber(a, 16) or 0 -- Create an alpha override code from a style definition colour code -alpha_from_style = (scolor) -> ass_alpha select 4, extract_color scolor +alpha_from_style = check'string' (scolor) -> ass_alpha select 4, extract_color scolor -- Create an colour override code from a style definition colour code -color_from_style = (scolor) -> +color_from_style = check'string' (scolor) -> r, g, b = extract_color scolor ass_color r or 0, g or 0, b or 0 diff --git a/automation/tests/modules/argcheck.moon b/automation/tests/modules/argcheck.moon new file mode 100644 index 000000000..72b888e15 --- /dev/null +++ b/automation/tests/modules/argcheck.moon @@ -0,0 +1,80 @@ +-- Copyright (c) 2014, Thomas Goyne +-- +-- Permission to use, copy, modify, and distribute this software for any +-- purpose with or without fee is hereby granted, provided that the above +-- copyright notice and this permission notice appear in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +-- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +-- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +-- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- +-- Aegisub Project http://www.aegisub.org/ + +check = require 'aegisub.argcheck' + +describe 'argcheck', -> + it 'should permit simple valid calls', -> + assert.has_no.errors -> (check'string' ->) '' + assert.has_no.errors -> (check'number' ->) 10 + assert.has_no.errors -> (check'boolean' ->) true + assert.has_no.errors -> (check'table' ->) {} + + it 'should support multiple arguments', -> + assert.has_no.errors -> (check'string number' ->) '', 10 + assert.has_no.errors -> (check'string table number' ->) '', {}, 10 + + it 'should support moonscript classes', -> + class Foo + assert.has_no.errors -> (check'Foo' (->) Foo) + + it 'should support optional arguments', -> + assert.has_no.errors -> (check'?number' ->) nil + assert.has_no.errors -> (check'?number ?number' ->) 5 + + it 'should support ...', -> + assert.has_no.errors -> (check'number ...' ->) 5 + assert.has_no.errors -> (check'number ...' ->) 5, 5 + assert.has_no.errors -> (check'number ...' ->) 5, 5, '' + + it 'should support alternates', -> + assert.has_no.errors -> (check'number|string' ->) 5 + assert.has_no.errors -> (check'number|string' ->) '' + + it 'should support optional alternates', -> + assert.has_no.errors -> (check'?number|string' ->) 5 + assert.has_no.errors -> (check'?number|string' ->) '' + assert.has_no.errors -> (check'?number|string' ->) nil + + it 'should reject simple invalid calls', -> + assert.has.errors -> (check'string' ->) 10 + assert.has.errors -> (check'number' ->) '' + + it 'should reject inccorect numbers of arguments', -> + assert.has.errors -> (check'string number' ->) '' + assert.has_no.errors -> (check'string ?number' ->) '' + assert.has.errors -> (check'string number' ->) '', 5, 5 + + it 'should reject non-optional nil arguments', -> + assert.has.errors -> (check'string number' ->) nil, nil + + it 'should reject invalid matches with alternates', -> + assert.has.errors -> (check'number|string' ->) {} + + it 'should report the correct error levels', -> + valid_err_loc = (fn) -> + _, err = pcall fn + err\find('tests/modules/argcheck.moon') != nil + + assert.is.true valid_err_loc -> (check'number' ->) {} + assert.is.true valid_err_loc -> (check'number' ->) nil + assert.is.true valid_err_loc -> (check'number|string' ->) {} + assert.is.true valid_err_loc -> (check'number|string' ->) nil + assert.is.true valid_err_loc -> (check'number string' ->) {} + assert.is.true valid_err_loc -> (check'?number ?string' ->) 1, 2, 3 + assert.is.true valid_err_loc -> (check'number string ...' ->) {} + prevent_tail_call_so_that_this_shows_up_in_backtrace = 1 +