diff --git a/config/locales/en.yml b/config/locales/en.yml index 73bc16a..cc40eb7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -27,8 +27,8 @@ en: parent_domain_mismatch: 'parent quantity has to be in the same domain' formula: invalid_formula: 'interpretation failed: %{msg}' - disallowed_token: 'includes disallowed token "%{token}" - (of type %{ttype}) at %{location}' + unparsable_formula: 'cannot be parsed' + disallowed_token: 'includes disallowed token "%{token}" (of type: %{ttype})' disallowed_function_call: 'includes disallowed function call: %{function}' unknown_quantity: 'contains undefined quantity: %{quantity}' body_trackers: diff --git a/lib/body_tracking/formula.rb b/lib/body_tracking/formula.rb index 275c317..ac05b00 100644 --- a/lib/body_tracking/formula.rb +++ b/lib/body_tracking/formula.rb @@ -10,6 +10,58 @@ module BodyTracking @formula = formula end + #def validate + # errors = [] + + # # 1st: check if formula is valid Ruby code + # #tokenized_length = Ripper.tokenize(@formula).join.length + # #unless tokenized_length == @formula.length + # # errors << [:invalid_formula, {part: @formula[0...tokenized_length]}] + # #end + # begin + # eval("-> { #{@formula} }") + # rescue ScriptError => e + # errors << [:invalid_formula, {msg: e.message}] + # end + + # # 2nd: check if formula contains only allowed token types + # # 3rd: check for disallowed function calls (they are not detected by Ripper.lex) + # # FIXME: this is unreliable (?) detection of function calls, should be replaced + # # with parsing Ripper.sexp if necessary + # identifiers = [] + # disallowed_functions = Set.new + # prev_ttype, prev_token = nil, nil + # Ripper.lex(@formula).each do |location, ttype, token| + # puts ttype, token + # case + # when QUANTITY_TTYPES.include?(prev_ttype) && ttype == :on_lparen + # disallowed_functions << prev_token unless FUNCTIONS.include?(prev_token) + # identifiers -= [prev_token] + # when prev_ttype == :on_period && QUANTITY_TTYPES.include?(ttype) + # disallowed_functions << token unless FUNCTIONS.include?(token) + # when is_token_quantity?(ttype, token) + # identifiers << token + # when [:on_sp, :on_int, :on_rational, :on_float, :on_tstring_beg, :on_tstring_end, + # :on_lparen, :on_rparen, :on_period].include?(ttype) + # when :on_op == ttype && + # ['+', '-', '*', '/', '%', '**', '==', '!=', '>', '<', '>=', '<=', '<=>', '===', + # '..', '...', '?:', 'and', 'or', 'not', '&&', '||', '!'].include?(token) + # when :on_kw == ttype && ['and', 'or', 'not'].include?(token) + # else + # errors << [:disallowed_token, {token: token, ttype: ttype, location: location}] + # end + # prev_ttype, prev_token = ttype, token unless ttype == :on_sp + # end + # disallowed_functions.each { |f| errors << [:disallowed_function_call, {function: f}] } + + # # 4th: check if identifiers used in formula correspond to existing quantities + # identifiers.uniq! + # quantities = @project.quantities.where(name: identifiers).pluck(:name) + # (identifiers - quantities).each { |q| errors << [:unknown_quantity, {quantity: q}] } + + # errors + #end + def validate errors = [] @@ -28,30 +80,70 @@ module BodyTracking # 3rd: check for disallowed function calls (they are not detected by Ripper.lex) # FIXME: this is unreliable (?) detection of function calls, should be replaced # with parsing Ripper.sexp if necessary + # failing test vectors: + # - fcall disallowed: "abs(Fats)+Energy < 10" + # working test vectors: + # a.abs(Fats)+Energy < 10 identifiers = [] disallowed_functions = Set.new - prev_ttype, prev_token = nil, nil - Ripper.lex(@formula).each do |location, ttype, token| - puts ttype, token - case - when QUANTITY_TTYPES.include?(prev_ttype) && ttype == :on_lparen - disallowed_functions << prev_token unless FUNCTIONS.include?(prev_token) - identifiers -= [prev_token] - when prev_ttype == :on_period && QUANTITY_TTYPES.include?(ttype) - disallowed_functions << token unless FUNCTIONS.include?(token) - when is_token_quantity?(ttype, token) - identifiers << token - when [:on_sp, :on_int, :on_rational, :on_float, :on_tstring_beg, :on_tstring_end, - :on_lparen, :on_rparen, :on_period].include?(ttype) - when :on_op == ttype && - ['+', '-', '*', '/', '%', '**', '==', '!=', '>', '<', '>=', '<=', '<=>', '===', - '..', '...', '?:', 'and', 'or', 'not', '&&', '||', '!'].include?(token) - when :on_kw == ttype && ['and', 'or', 'not'].include?(token) + + stree = [Ripper.sexp(@formula)] + errors << [:unparsable_formula, {}] unless stree.first + + while stree.first + ttype, token, *rest = stree.shift + case ttype + when :program, :args_add_block + stree.unshift(*token) + when :binary + operator, token2 = rest + stree.unshift(token, token2) + when :method_add_arg + stree.unshift(token, *rest) + when :call + stree.unshift(token) + dot, method = rest + ftype, fname, floc = method + disallowed_functions << fname unless FUNCTIONS.include?(fname) + when :fcall + ftype, fname, floc = token + disallowed_functions << fname + when :vcall + ftype, fname, floc = token + identifiers << fname + when :arg_paren + stree.unshift(token) + when :var_ref + vtype, vname, vloc = token + identifiers << vname + when :@int else - errors << [:disallowed_token, {token: token, ttype: ttype, location: location}] + byebug + errors << [:disallowed_token, {token: token, ttype: ttype}] end - prev_ttype, prev_token = ttype, token unless ttype == :on_sp end + + #Ripper.lex(@formula).each do |location, ttype, token| + # puts ttype, token + # case + # when QUANTITY_TTYPES.include?(prev_ttype) && ttype == :on_lparen + # disallowed_functions << prev_token unless FUNCTIONS.include?(prev_token) + # identifiers -= [prev_token] + # when prev_ttype == :on_period && QUANTITY_TTYPES.include?(ttype) + # disallowed_functions << token unless FUNCTIONS.include?(token) + # when is_token_quantity?(ttype, token) + # identifiers << token + # when [:on_sp, :on_int, :on_rational, :on_float, :on_tstring_beg, :on_tstring_end, + # :on_lparen, :on_rparen, :on_period].include?(ttype) + # when :on_op == ttype && + # ['+', '-', '*', '/', '%', '**', '==', '!=', '>', '<', '>=', '<=', '<=>', '===', + # '..', '...', '?:', 'and', 'or', 'not', '&&', '||', '!'].include?(token) + # when :on_kw == ttype && ['and', 'or', 'not'].include?(token) + # else + # errors << [:disallowed_token, {token: token, ttype: ttype, location: location}] + # end + # prev_ttype, prev_token = ttype, token unless ttype == :on_sp + #end disallowed_functions.each { |f| errors << [:disallowed_function_call, {function: f}] } # 4th: check if identifiers used in formula correspond to existing quantities @@ -81,7 +173,8 @@ module BodyTracking inputs.map do |i, values| begin [i, [get_binding(values).eval(paramed_formula), nil]] - rescue Exception + rescue Exception => e + puts e.message [i, [nil, nil]] end end