module NeuronCheckSystem

Constants

ATTR_DECLARATIONS

全属性制限と対応するメソッド情報のリスト

LOCAL_VARIABLE_GET_USABLE

bindingクラスに local_variable_get が定義されているかどうかを判定し、その判定結果を定数に記録 (Ruby 2.1以降では定義されているはず)

METHOD_DECLARATIONS

全メソッド宣言のリスト

RUBY_TOPLEVEL
TRACE_POINT

チェック処理を差し込むためのTracePointを作成

Public Class Methods

get_appropriate_matcher(expected, declared_caller_locations) click to toggle source

期待する値に対応する、適切なマッチャを取得

# File lib/neuroncheck/matcher.rb, line 7
def self.get_appropriate_matcher(expected, declared_caller_locations)
  case expected
  when DeclarationContext # 誤って「self」と記載した場合
    raise DeclarationError, "`self` cannot be used in declaration - use `:self` instead"
  when :self # self
    SelfMatcher.new(declared_caller_locations) # 値がselfであるかどうかチェック

  when String, Symbol, Integer
    ValueEqualMatcher.new(expected, declared_caller_locations) # 値が等しいかどうかをチェック
  when true, false, nil
    ObjectIdenticalMathcer.new(expected, declared_caller_locations) # オブジェクトが同一かどうかをチェック
  when Class, Module
    KindOfMatcher.new(expected, declared_caller_locations) # 所属/継承しているかどうかをチェック
  when Range
    RangeMatcher.new(expected, declared_caller_locations) # 範囲チェック
  when Regexp
    RegexpMatcher.new(expected, declared_caller_locations) # 正規表現チェック
  # when Encoding
  #   EncodingMatcher.new(expected, declared_caller_locations) # エンコーディングチェック

  when Array
    OrMatcher.new(expected, declared_caller_locations) # ORチェック

  when Plugin::Keyword # プラグインによって登録されたキーワードの場合
    KeywordPluginMatcher.new(expected, declared_caller_locations)

  else
    raise DeclarationError, "#{expected.class.name} cannot be usable for NeuronCheck check parameter\n  value: #{expected.inspect}"
  end

end
get_local_variable_from_binding(target_binding, var_name) click to toggle source

指定したbindingから、指定した名前のローカル変数を取得する (Binding#local_variable_getが定義されているかどうかで実装を変える)

# File lib/neuroncheck/kernel.rb, line 256
def self.get_local_variable_from_binding(target_binding, var_name)
  if LOCAL_VARIABLE_GET_USABLE then
    target_binding.local_variable_get(var_name)
  else
    # local_variable_getが使えない時は、代わりにevalを使用して取得
    target_binding.eval(var_name.to_s)
  end
end
initialize_module_for_neuron_check(mod_or_class) click to toggle source

モジュール/クラス1つに対して、NeuronCheck用の初期化を行う

# File lib/neuroncheck/kernel.rb, line 431
def self.initialize_module_for_neuron_check(mod_or_class)
  # 2回目以降の初期化であれば何もせずにスルー
  if mod_or_class.instance_variable_get(:@__neuron_check_initialized) then
    return
  end

  # 宣言とメソッド情報を格納するためのHashを更新
  NeuronCheckSystem::METHOD_DECLARATIONS[mod_or_class] = {}
  NeuronCheckSystem::METHOD_DECLARATIONS[mod_or_class.singleton_class] = {} # 特異メソッド用のクラスも追加
  NeuronCheckSystem::ATTR_DECLARATIONS[mod_or_class] = {}

  # 対象のModule/Classに対する処理
  mod_or_class.instance_eval do
    # 最後に宣言された内容を保持するクラスインスタンス変数(モジュールインスタンス変数)を定義
    @__neuron_check_last_declaration = nil
    # メソッド定義時のフックを一時的に無効化するフラグ
    @__neuron_check_method_added_hook_enabled = true
    # 属性の処理を上書きするために差し込む無名モジュールを定義 (prependする)
    @__neuron_check_attr_check_module = Module.new
    prepend @__neuron_check_attr_check_module

    # メソッド定義時のフックなどを定義したモジュールをextend
    extend NeuronCheckSystem::Kernel

    # initialize済みフラグON
    @__neuron_check_initialized = true # 初期化
  end
end
make_cond_block_context(method_self, context_caption, param_values, allow_instance_method) click to toggle source

事前条件/事後条件の実行用コンテキストを構築する

# File lib/neuroncheck/kernel.rb, line 405
def self.make_cond_block_context(method_self, context_caption, param_values, allow_instance_method)
  # ローカル変数指定用の無名モジュールを作成
  local_mod = Module.new
  local_mod.module_eval do
    # ローカル変数取得用のメソッドを定義する
    param_values.each_pair do |var_name, value|
      define_method(var_name) do
        return value
      end
    end
  end

  # ブロック実行用のコンテキストを生成し、ローカル変数保持モジュールを差し込む
  context = CondBlockContext.new(context_caption, method_self, allow_instance_method)
  context.extend local_mod

  # 対象のオブジェクトが持つ全てのインスタンス変数を、コンテキストへ引き渡す
  method_self.instance_variables.each do |var_name|
    val = method_self.instance_variable_get(var_name)
    context.instance_variable_set(var_name, val)
  end

  return context
end
trace_method_call(method_self, declaration, met, method_binding) click to toggle source

メソッド呼び出し時のチェック処理 (現段階ではattrメソッドの呼び出しでは発生しないことに注意。Ruby 2.1で確認)

# File lib/neuroncheck/kernel.rb, line 266
def self.trace_method_call(method_self, declaration, met, method_binding)
  param_values = {}

  # 大域脱出できるようにcatchを使用 (バックトレースを正しく取得するための処置)
  error_msg = catch(:neuron_check_error_tag) do

    # 対象メソッドの引数1つごとに処理
    met.parameters.each_with_index do |param_info, def_param_index|
      param_type, param_name = param_info

      # 実際に渡された引数の値を取得 (デフォルト値の処理も考慮する)
      param_value = get_local_variable_from_binding(method_binding, param_name)
      param_values[param_name] = param_value

      # 指定位置に対応するマッチャが登録されている場合のみ処理
      if (matcher = declaration.arg_matchers[def_param_index]) then

        # 引数の種類で処理を分岐
        case param_type
        when :key # キーワード引数
          unless matcher.match?(param_value, self) then
            context_caption = "argument `#{param_name}' of `#{declaration.signature_caption_name_only}'"
            @last_argument_error_info = [method_self, met.name] # 引数チェックエラーの発生情報を記録

            throw :neuron_check_error_tag, matcher.get_error_message(declaration, context_caption, param_value)
          end

        when :keyrest # キーワード引数の可変長部分
          # 可変長部分1つごとにチェック
          param_value.each_pair do |key, value|
            unless matcher.match?(value, self) then
              context_caption = "argument `#{param_name}' of `#{declaration.signature_caption_name_only}'"
              @last_argument_error_info = [method_self, met.name] # 引数チェックエラーの発生情報を記録

              throw :neuron_check_error_tag, matcher.get_error_message(declaration, context_caption, value)
            end
          end

        when :rest # 可変長部分
          # 可変長引数であれば、受け取った引数1つごとにチェック
          param_value.each_with_index do |val, i|
            unless matcher.match?(val, self) then
              context_caption = "#{NeuronCheckSystem::Utils.ordinalize(def_param_index + 1 + i)} argument `#{param_name}' of `#{declaration.signature_caption_name_only}'"
              @last_argument_error_info = [method_self, met.name] # 引数チェックエラーの発生情報を記録

              throw :neuron_check_error_tag, matcher.get_error_message(declaration, context_caption, val)
            end
          end

        else # 通常の引数/ブロック引数
          unless matcher.match?(param_value, self) then
            context_caption = "#{NeuronCheckSystem::Utils.ordinalize(def_param_index + 1)} argument `#{param_name}' of `#{declaration.signature_caption_name_only}'"
            @last_argument_error_info = [method_self, met.name] # 引数チェックエラーの発生情報を記録

            throw :neuron_check_error_tag, matcher.get_error_message(declaration, context_caption, param_value)
          end
        end
      end
    end


    # 事前条件があれば実行
    if declaration.precond then
      # コンテキストを生成
      context = make_cond_block_context(method_self, 'precond', param_values, declaration.precond_allow_instance_method)

      # 条件文実行
      context.instance_exec(&(declaration.precond))
    end

    # 最後まで正常実行した場合はnilを返す
    nil
  end # catch

  # エラーメッセージがあればNeuronCheckError発生
  if error_msg then
    raise NeuronCheckError, error_msg, (NeuronCheck.debug? ? caller : caller(3))
  end
end
trace_method_return(method_self, declaration, met, method_binding, return_value) click to toggle source

メソッド終了時のチェック処理

# File lib/neuroncheck/kernel.rb, line 347
def self.trace_method_return(method_self, declaration, met, method_binding, return_value)
  # 処理中に例外が発生した場合、TracePointがreturnとして検知してしまうため
  # それを防ぐために例外を自前でキャッチ
  begin
    # 大域脱出できるようにcatchを使用 (バックトレースを正しく取得するための処置)
    error_msg = catch(:neuron_check_error_tag) do

      # 最後に引数チェックエラーが発生しており、selfとメソッド名が同じものであれば、return値のチェックをスキップ
      # (NeuronCheckError例外を発生させたときにもreturnイベントは発生してしまうため、その対策として)
      if @last_argument_error_info and @last_argument_error_info[0].equal?(method_self) and @last_argument_error_info[1] == met.name then
        @last_argument_error_info = nil
        return
      end

      param_values = {}

      # 対象メソッドの引数1つごとに処理
      met.parameters.each_with_index do |param_info, def_param_index|
        _, param_name = param_info

        # 実際に渡された引数の値を取得 (デフォルト値の処理も考慮する)
        param_value = get_local_variable_from_binding(method_binding, param_name)
        param_values[param_name] = param_value
      end

      # 戻り値チェック
      if (matcher = declaration.return_matcher) then
        # チェック処理
        unless matcher.match?(return_value, method_self) then
          # エラー
          throw :neuron_check_error_tag, matcher.get_error_message(declaration, "return value of `#{declaration.signature_caption_name_only}'", return_value)
        end
      end

      # 事後条件があれば実行
      if declaration.postcond then
        # コンテキストを生成
        context = make_cond_block_context(method_self, 'postcond', param_values, declaration.postcond_allow_instance_method)

        # 条件文実行
        context.instance_exec(return_value, &(declaration.postcond))
      end

      # 最後まで正常実行した場合はnilを返す
      nil
    end # catch

    # エラーメッセージがあればNeuronCheckError発生
    if error_msg then
      raise NeuronCheckError, error_msg, (NeuronCheck.debug? ? caller : caller(3))
    end
  rescue Exception
    # 例外が発生した場合はその例外オブジェクトを返す
    return $!
  end
end