VirtualTimeMigrate (GenServerVirtualTimeMigrate v1.1.1)

Copy Markdown View Source

Works out how to turn GenServer modules into their virtual-time equivalents.

It is a planner, not a formatter: it reads a file, decides which tokens to replace and where, and hands the plan back. mix virtual_time.migrate decides whether to print that plan or apply it.

The plan is deliberately conservative — a call is only rewritten when the change provably means the same thing:

  • use GenServer becomes use VirtualTimeGenServer, options untouched (use GenStateMachine → use VirtualTimeGenStateMachine);
  • inside a converted module, Process.send_after/3, Process.sleep/1 and :timer.sleep/1 become VirtualTimeGenServer calls, because that is what puts the module on the virtual clock;
  • GenServer.call/cast/stop/reply/start_link/start/child_spec is rewritten only when its target is a literal module that is converted somewhere in the project, or __MODULE__ inside a converted module.

Anything else — a pid, a variable, a module that stays a plain GenServer — is left alone, and reported when the caller is a converted module and the tool cannot tell. :erlang.send_after/3 is in that group: it takes the delay first where the virtual-time call takes it last, and swapping arguments is not a token replacement, so it is reported for a human to reorder.

Every edit carries the exact source text it expects to find at its position, and is checked against the source before it joins the plan, so a plan can never be applied to text it does not describe.

Summary

Types

One replacement: from is the exact source text at column on line, and to is what replaces it.

Something the migration noticed but does not rewrite.

Functions

Applies a plan to the source it was planned from.

The modules a source (or an AST) converts, as dotted names.

Plans the migration of one file's source.

Types

edit()

@type edit() :: %{
  line: pos_integer(),
  column: pos_integer(),
  from: String.t(),
  to: String.t(),
  change: String.t()
}

One replacement: from is the exact source text at column on line, and to is what replaces it.

note()

@type note() :: %{line: pos_integer(), message: String.t()}

Something the migration noticed but does not rewrite.

plan()

@type plan() :: %{edits: [edit()], notes: [note()]}

Functions

apply_edits(source, edits)

@spec apply_edits(String.t(), [edit()]) :: String.t()

Applies a plan to the source it was planned from.

Edits are applied right to left within a line, so an earlier replacement never moves a later one.

converted_modules(source)

@spec converted_modules(Macro.t() | String.t()) :: MapSet.t(String.t())

The modules a source (or an AST) converts, as dotted names.

plan(source, known_converted \\ nil)

@spec plan(String.t(), MapSet.t(String.t()) | nil) :: {:ok, plan()} | {:error, term()}

Plans the migration of one file's source.

known_converted is the set of module names the rest of the project converts (see converted_modules/1), so a call into another file can be recognised. The modules this file converts are added to it.

Returns {:error, reason} when the source does not parse — the caller reports that file and carries on with the others.