`Workflow.add_cascade` dialyzer error when adding deps

When adding :deps to Workflow.add_cascade/4 I get a LSP/Dialyzer error

The function call will not succeed.

Oban.Pro.Workflow.add_cascade(
  %Oban.Pro.Workflow{
    :changesets => [
      %Ecto.Changeset{
        :action => atom(),
        :changes => %{atom() => _},
        :constraints => [
          %{
            :constraint =>
              binary()
              | %Regex{
                  :opts => binary() | [any()],
                  :re_pattern => _,
                  :re_version => _,
                  :source => binary()
                },
            :error_message => binary(),
            :error_type => atom(),
            :field => atom(),
            :match => :exact | :prefix | :suffix,
            :type => :check | :exclusion | :foreign_key | :unique
          }
        ],
        :data => %Oban.Job{
          :__meta__ => _,
          :args => map(),
          :attempt => non_neg_integer(),
          :attempted_at =>
            nil
            | %DateTime{
                :calendar => atom(),
                :day => pos_integer(),
                :hour => non_neg_integer(),
                :microsecond => {non_neg_integer(), non_neg_integer()},
                :minute => non_neg_integer(),
                :month => pos_integer(),
                :second => non_neg_integer(),
                :std_offset => integer(),
                :time_zone => binary(),
                :utc_offset => integer(),
                :year => integer(),
                :zone_abbr => binary()
              },
          :attempted_by => nil | [binary()],
          :cancelled_at =>
            nil
            | %DateTime{
                :calendar => atom(),
                :day => pos_integer(),
                :hour => non_neg_integer(),
                :microsecond => {non_neg_integer(), non_neg_integer()},
                :minute => non_neg_integer(),
                :month => pos_integer(),
                :second => non_neg_integer(),
                :std_offset => integer(),
                :time_zone => binary(),
                :utc_offset => integer(),
                :year => integer(),
                :zone_abbr => binary()
              },
          :completed_at =>
            nil
            | %DateTime{
                :calendar => atom(),
                :day => pos_integer(),
                :hour => non_neg_integer(),
                :microsecond => {non_neg_integer(), non_neg_integer()},
                :minute => non_neg_integer(),
                :month => pos_integer(),
                :second => non_neg_integer(),
                :std_offset => integer(),
                :time_zone => binary(),
                :utc_offset => integer(),
                :year => integer(),
                :zone_abbr => binary()
              },
          :conf =>
            nil
            | %Oban.Config{
                :dispatch_cooldown => pos_integer(),
                :engine => atom(),
                :get_dynamic_repo => nil | (-> atom() | pid()) | {atom(), atom(), [any()]},
                :insert_trigger => boolean(),
                :log =>
                  :alert
                  | :critical
                  | :debug
                  | :emergency
                  | :error
                  | false
                  | :info
                  | :notice
                  | :warn
                  | :warning,
                :name => _,
                :node => binary(),
                :notifier => {atom(), Keyword.t()},
                :peer => {atom(), Keyword.t()},
                :plugins => [atom() | {atom() | Keyword.t()}],
                :prefix => false | binary(),
                :queues => Keyword.t(Keyword.t()),
                :repo => atom(),
                :shutdown_grace_period => non_neg_integer(),
                :stage_interval => timeout(),
                :testing => :disabled | :inline | :manual
              },
          :conflict? => boolean(),
          :discarded_at =>
            nil
            | %DateTime{
                :calendar => atom(),
                :day => pos_integer(),
                :hour => non_neg_integer(),
                :microsecond => {non_neg_integer(), non_neg_integer()},
                :minute => non_neg_integer(),
                :month => pos_integer(),
                :second => non_neg_integer(),
                :std_offset => integer(),
                :time_zone => binary(),
                :utc_offset => integer(),
                :year => integer(),
                :zone_abbr => binary()
              },
          :errors => [
            %{
              :at => %DateTime{
                :calendar => atom(),
                :day => pos_integer(),
                :hour => non_neg_integer(),
                :microsecond => {non_neg_integer(), non_neg_integer()},
                :minute => non_neg_integer(),
                :month => pos_integer(),
                :second => non_neg_integer(),
                :std_offset => integer(),
                :time_zone => binary(),
                :utc_offset => integer(),
                :year => integer(),
                :zone_abbr => binary()
              },
              :attempt => pos_integer(),
              :error => binary()
            }
          ],
          :id => pos_integer(),
          :inserted_at => %DateTime{
            :calendar => atom(),
            :day => pos_integer(),
            :hour => non_neg_integer(),
            :microsecond => {non_neg_integer(), non_neg_integer()},
            :minute => non_neg_integer(),
            :month => pos_integer(),
            :second => non_neg_integer(),
            :std_offset => integer(),
            :time_zone => binary(),
            :utc_offset => integer(),
            :year => integer(),
            :zone_abbr => binary()
          },
          :max_attempts => pos_integer(),
          :meta => map(),
          :priority => 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9,
          :queue => binary(),
          :replace =>
            nil
            | [
                [
                  :args
                  | :max_attempts
                  | :meta
                  | :priority
                  | :queue
                  | :scheduled_at
                  | :tags
                  | :worker
                ]
                | {:available
                   | :cancelled
                   | :completed
                   | :discarded
                   | :executing
                   | :retryable
                   | :scheduled,
                   [
                     [
                       :args
                       | :max_attempts
                       | :meta
                       | :priority
                       | :queue
                       | :scheduled_at
                       | :tags
                       | :worker
                     ]
                   ]}
              ],
          :scheduled_at => %DateTime{
            :calendar => atom(),
            :day => pos_integer(),
            :hour => non_neg_integer(),
            :microsecond => {non_neg_integer(), non_neg_integer()},
            :minute => non_neg_integer(),
            :month => pos_integer(),
            :second => non_neg_integer(),
            :std_offset => integer(),
            :time_zone => binary(),
            :utc_offset => integer(),
            :year => integer(),
            :zone_abbr => binary()
          },
          :state => binary(),
          :tags => [binary()],
          :unique =>
            nil
            | %{
                :fields => [:args | :meta | :queue | :worker],
                :keys => [atom()],
                :period =>
                  :infinity
                  | pos_integer()
                  | {pos_integer(),
                     :day
                     | :days
                     | :hour
                     | :hours
                     | :minute
                     | :minutes
                     | :second
                     | :seconds
                     | :week
                     | :weeks},
                :states => [
                  [
                    :available
                    | :cancelled
                    | :completed
                    | :discarded
                    | :executing
                    | :retryable
                    | :scheduled
                  ]
                ],
                :timestamp => :inserted_at | :scheduled_at
              },
          :unsaved_error =>
            nil
            | %{
                :kind => :error | :exit | :throw | {:EXIT, pid()},
                :reason => _,
                :stacktrace => [
                  {(... -> any), [any()] | non_neg_integer(), Keyword.t()}
                  | {atom(), atom(), [any()] | non_neg_integer(), Keyword.t()}
                ]
              },
          :worker => binary()
        },
        :empty_values => _,
        :errors => Keyword.t({binary(), Keyword.t()}),
        :filters => %{atom() => _},
        :params => nil | %{binary() => _},
        :prepare => [
          (%Ecto.Changeset{:action => atom(), :changes => map(), _ => _} ->
             %Ecto.Changeset{:action => atom(), :changes => map(), _ => _})
        ],
        :repo => atom(),
        :repo_opts => Keyword.t(),
        :required => [atom()],
        :types => %{
          atom() =>
            atom()
            | {:array | :assoc | :embed | :in | :map | :parameterized | :supertype | :try,
               _}
        },
        :valid? => boolean(),
        :validations => Keyword.t()
      },
      ...
    ],
    :check_deps => boolean(),
    :grafts => _,
    :id => binary(),
    :names => %MapSet{:map => MapSet.internal(_) | :sets.set(_)},
    :opts => map(),
    :subs => map()
  },
  :orders,
  (_ -> {[any()], [any()]}),
  [{:deps, :init}]
)

breaks the contract
(t(), name(), cascade_capture(), add_cascade_opts()) :: t()

I believe there’s a bug in the type for add_cascade_opts():

@type add_cascade_opts() :: [Oban.Job.option() | add_opts()]

@type add_opts() :: [
  deps: name() | [name()],
  ignore_cancelled: boolean(),
  ignore_deleted: boolean(),
  ignore_discarded: boolean()
]

The | operator normally expects options also separated with |, like Oban.Job.option():

@type option() ::
  {:args, args()}
  | {:max_attempts, pos_integer()}
  | {:meta, map()}
  | {:priority, 0..9}
  | etc

As currently written, the type means “a list where elements are either tuples from Oban.Job.option() or a keyword list shaped like add_opts()”, so something like [[deps: :init]] would match the current spec. (but presumably fail catastrophically where options are parsed)

I don’t currently have access to Oban Pro, but updating the definition of add_cascade_opts would likely remove the error you’re seeing:

@type add_cascade_opts() :: [Oban.Job.option() | add_opt()]

@type add_opts() :: [add_opt()]

@type add_opt() ::
  {:deps, name() | [name()]}
  | {:ignore_cancelled, boolean()}
  | {:ignore_deleted, boolean()}
  | {:ignore_discarded, boolean()}

Beware that changes to files in deps/ won’t get picked up by Dialyzer until you nuke _build and any cached PLTs.

3 Likes

Thanks for reporting the issue (and thanks @al2o3cr for the resolution). This is fixed for the Pro v1.6 release :slightly_smiling_face:

2 Likes