Current section
26 Versions
Jump to
Current section
26 Versions
Compare versions
10
files changed
+211
additions
-676
deletions
| @@ -467,8 +467,8 @@ const Erlang = { | |
| 467 467 | ); |
| 468 468 | }, |
| 469 469 | // End binary_part/3 |
| 470 | - |
| 471 470 | // Deps: [] |
| 471 | + |
| 472 472 | // Start binary_to_atom/1 |
| 473 473 | "binary_to_atom/1": (binary) => { |
| 474 474 | return Erlang["binary_to_atom/2"](binary, Type.atom("utf8")); |
| @@ -8,7 +8,7 @@ | |
| 8 8 | {<<"Sponsor">>,<<"https://github.com/sponsors/bartblast">>}, |
| 9 9 | {<<"Website">>,<<"https://hologram.page">>}]}. |
| 10 10 | {<<"name">>,<<"hologram">>}. |
| 11 | - {<<"version">>,<<"0.8.2">>}. |
| 11 | + {<<"version">>,<<"0.8.3">>}. |
| 12 12 | {<<"description">>, |
| 13 13 | <<"Full stack isomorphic Elixir web framework that can be used on top of Phoenix.">>}. |
| 14 14 | {<<"elixir">>,<<"~> 1.0">>}. |
| @@ -75,8 +75,6 @@ | |
| 75 75 | <<"lib/mix/tasks/holo/compiler/ex_runtime_mfas.ex">>, |
| 76 76 | <<"lib/mix/tasks/holo/compiler/runtime_to_mfa_paths.ex">>, |
| 77 77 | <<"lib/mix/tasks/holo/compiler/page_ex_fun_sizes.ex">>, |
| 78 | - <<"lib/mix/tasks/holo/compiler/runtime_mfa_cascades.ex">>, |
| 79 | - <<"lib/mix/tasks/holo/compiler/page_mfa_cascades.ex">>, |
| 80 78 | <<"lib/mix/tasks/compile">>,<<"lib/mix/tasks/compile/hologram.ex">>, |
| 81 79 | <<"lib/hologram">>,<<"lib/hologram/reflection.ex">>,<<"lib/hologram/ui">>, |
| 82 80 | <<"lib/hologram/ui/runtime.ex">>,<<"lib/hologram/ui/link.ex">>, |
| @@ -91,12 +91,10 @@ defmodule Hologram.Compiler do | |
| 91 91 | |
| 92 92 | ir_plt |
| 93 93 | |> PLT.get_all() |
| 94 | - |> TaskUtils.async_many(fn {_module, ir} -> |
| 95 | - CallGraph.build(call_graph, ir, %CallGraph.Context{}) |
| 96 | - end) |
| 94 | + |> TaskUtils.async_many(fn {_module, ir} -> CallGraph.build(call_graph, ir) end) |
| 97 95 | |> Task.await_many(:infinity) |
| 98 96 | |
| 99 | - call_graph |
| 97 | + CallGraph.add_non_discoverable_edges(call_graph) |
| 100 98 | end |
| 101 99 | |
| 102 100 | @doc """ |
| @@ -5,41 +5,10 @@ defmodule Hologram.Compiler.CallGraph do | |
| 5 5 | alias Hologram.Commons.SerializationUtils |
| 6 6 | alias Hologram.Commons.TaskUtils |
| 7 7 | alias Hologram.Compiler.CallGraph |
| 8 | - alias Hologram.Compiler.CallGraph.Context |
| 9 8 | alias Hologram.Compiler.Digraph |
| 10 9 | alias Hologram.Compiler.IR |
| 11 10 | alias Hologram.Reflection |
| 12 11 | |
| 13 | - defmodule Context.Modifiers do |
| 14 | - @moduledoc false |
| 15 | - |
| 16 | - @type t :: %__MODULE__{ |
| 17 | - suppress_edges_to_module_vertices?: bool |
| 18 | - } |
| 19 | - |
| 20 | - defstruct suppress_edges_to_module_vertices?: false |
| 21 | - end |
| 22 | - |
| 23 | - defmodule Context do |
| 24 | - @moduledoc false |
| 25 | - |
| 26 | - alias Hologram.Compiler.CallGraph.Context.Modifiers |
| 27 | - |
| 28 | - @type t :: %__MODULE__{ |
| 29 | - from_vertex: module | mfa | nil, |
| 30 | - guard?: bool, |
| 31 | - modifiers: Modifiers.t(), |
| 32 | - pattern?: bool, |
| 33 | - protocol_impl: module | nil |
| 34 | - } |
| 35 | - |
| 36 | - defstruct from_vertex: nil, |
| 37 | - guard?: false, |
| 38 | - modifiers: %Modifiers{}, |
| 39 | - pattern?: false, |
| 40 | - protocol_impl: nil |
| 41 | - end |
| 42 | - |
| 43 12 | defstruct pid: nil |
| 44 13 | |
| 45 14 | @type t :: %CallGraph{pid: pid} |
| @@ -47,8 +16,52 @@ defmodule Hologram.Compiler.CallGraph do | |
| 47 16 | @type edge :: {vertex, vertex} |
| 48 17 | @type vertex :: module | mfa |
| 49 18 | |
| 50 | - # Page functions needed by other pages for link building (e.g. in Hologram.UI.Link component). |
| 51 | - @cross_page_funs [{:__params__, 0}, {:__route__, 0}] |
| 19 | + # These edges can't be discovered from static IR analysis. |
| 20 | + @dynamic_dispatch_edges [ |
| 21 | + {{Date, :day_of_era, 1}, {Calendar.ISO, :day_of_era, 3}}, |
| 22 | + {{Date, :day_of_week, 2}, {Calendar.ISO, :day_of_week, 4}}, |
| 23 | + {{Date, :day_of_year, 1}, {Calendar.ISO, :day_of_year, 3}}, |
| 24 | + {{Date, :days_in_month, 1}, {Calendar.ISO, :days_in_month, 2}}, |
| 25 | + {{Date, :leap_year?, 1}, {Calendar.ISO, :leap_year?, 1}}, |
| 26 | + {{Date, :months_in_year, 1}, {Calendar.ISO, :months_in_year, 1}}, |
| 27 | + {{Date, :new, 4}, {Calendar.ISO, :valid_date?, 3}}, |
| 28 | + {{Date, :quarter_of_year, 1}, {Calendar.ISO, :quarter_of_year, 3}}, |
| 29 | + {{Date, :shift, 2}, {Calendar.ISO, :shift_date, 4}}, |
| 30 | + {{Date, :to_string, 1}, {Calendar.ISO, :date_to_string, 3}}, |
| 31 | + {{Date, :year_of_era, 1}, {Calendar.ISO, :year_of_era, 3}}, |
| 32 | + {{DateTime, :from_gregorian_seconds, 3}, {Calendar.ISO, :naive_datetime_from_iso_days, 1}}, |
| 33 | + {{DateTime, :from_iso_days, 4}, {Calendar.ISO, :naive_datetime_from_iso_days, 1}}, |
| 34 | + {{DateTime, :shift, 3}, {Calendar.ISO, :naive_datetime_to_iso_days, 7}}, |
| 35 | + {{DateTime, :shift, 3}, {Calendar.ISO, :shift_naive_datetime, 8}}, |
| 36 | + {{DateTime, :shift_by_offset, 2}, {Calendar.ISO, :naive_datetime_from_iso_days, 1}}, |
| 37 | + {{DateTime, :shift_zone_for_iso_days_utc, 5}, |
| 38 | + {Calendar.ISO, :naive_datetime_from_iso_days, 1}}, |
| 39 | + {{DateTime, :to_iso_days, 1}, {Calendar.ISO, :naive_datetime_to_iso_days, 7}}, |
| 40 | + {{DateTime, :to_string, 1}, {Calendar.ISO, :datetime_to_string, 11}}, |
| 41 | + {{Inspect.Date, :inspect, 2}, {Calendar.ISO, :date_to_string, 3}}, |
| 42 | + {{Inspect.DateTime, :inspect, 2}, {Calendar.ISO, :datetime_to_string, 11}}, |
| 43 | + {{Inspect.NaiveDateTime, :inspect, 2}, {Calendar.ISO, :naive_datetime_to_string, 7}}, |
| 44 | + {{Inspect.Time, :inspect, 2}, {Calendar.ISO, :time_to_string, 4}}, |
| 45 | + {{NaiveDateTime, :beginning_of_day, 1}, {Calendar.ISO, :iso_days_to_beginning_of_day, 1}}, |
| 46 | + {{NaiveDateTime, :end_of_day, 1}, {Calendar.ISO, :iso_days_to_end_of_day, 1}}, |
| 47 | + {{NaiveDateTime, :from_iso_days, 3}, {Calendar.ISO, :naive_datetime_from_iso_days, 1}}, |
| 48 | + {{NaiveDateTime, :new, 8}, {Calendar.ISO, :valid_date?, 3}}, |
| 49 | + {{NaiveDateTime, :new, 8}, {Calendar.ISO, :valid_time?, 4}}, |
| 50 | + {{NaiveDateTime, :shift, 2}, {Calendar.ISO, :shift_naive_datetime, 8}}, |
| 51 | + {{NaiveDateTime, :to_gregorian_seconds, 1}, {Calendar.ISO, :naive_datetime_to_iso_days, 7}}, |
| 52 | + {{NaiveDateTime, :to_iso_days, 1}, {Calendar.ISO, :naive_datetime_to_iso_days, 7}}, |
| 53 | + {{NaiveDateTime, :to_string, 1}, {Calendar.ISO, :naive_datetime_to_string, 7}}, |
| 54 | + {{String.Chars.Date, :to_string, 1}, {Calendar.ISO, :date_to_string, 3}}, |
| 55 | + {{String.Chars.DateTime, :to_string, 1}, {Calendar.ISO, :datetime_to_string, 11}}, |
| 56 | + {{String.Chars.NaiveDateTime, :to_string, 1}, {Calendar.ISO, :naive_datetime_to_string, 7}}, |
| 57 | + {{String.Chars.Time, :to_string, 1}, {Calendar.ISO, :time_to_string, 4}}, |
| 58 | + {{Time, :convert, 2}, {Calendar.ISO, :time_from_day_fraction, 1}}, |
| 59 | + {{Time, :from_seconds_after_midnight, 3}, {Calendar.ISO, :time_from_day_fraction, 1}}, |
| 60 | + {{Time, :new, 5}, {Calendar.ISO, :valid_time?, 4}}, |
| 61 | + {{Time, :shift, 2}, {Calendar.ISO, :shift_time, 5}}, |
| 62 | + {{Time, :to_day_fraction, 1}, {Calendar.ISO, :time_to_day_fraction, 4}}, |
| 63 | + {{Time, :to_string, 1}, {Calendar.ISO, :time_to_string, 4}} |
| 64 | + ] |
| 52 65 | |
| 53 66 | # TODO: Determine automatically based on deps annotations next to function implementations |
| 54 67 | @erlang_mfa_edges [ |
| @@ -332,6 +345,22 @@ defmodule Hologram.Compiler.CallGraph do | |
| 332 345 | call_graph |
| 333 346 | end |
| 334 347 | |
| 348 | + @doc """ |
| 349 | + Adds call graph edges that can't be discovered from static IR analysis: |
| 350 | + Erlang functions depending on other Erlang functions, and dynamic dispatch |
| 351 | + patterns in Elixir stdlib (e.g. behaviour callbacks called via variable with known default). |
| 352 | + """ |
| 353 | + @spec add_non_discoverable_edges(t) :: t |
| 354 | + def add_non_discoverable_edges(%{pid: pid} = call_graph) do |
| 355 | + Agent.cast(pid, fn graph -> |
| 356 | + graph |
| 357 | + |> Digraph.add_edges(@erlang_mfa_edges) |
| 358 | + |> Digraph.add_edges(@dynamic_dispatch_edges) |
| 359 | + end) |
| 360 | + |
| 361 | + call_graph |
| 362 | + end |
| 363 | + |
| 335 364 | @doc """ |
| 336 365 | Adds the vertex to the call graph. |
| 337 366 | """ |
| @@ -344,14 +373,10 @@ defmodule Hologram.Compiler.CallGraph do | |
| 344 373 | @doc """ |
| 345 374 | Builds a call graph from IR. |
| 346 375 | """ |
| 347 | - @spec build(t, IR.t() | list, Context.t()) :: t |
| 376 | + @spec build(t, IR.t() | list | map | tuple, vertex | nil) :: t |
| 377 | + def build(call_graph, ir, from_vertex \\ nil) |
| 348 378 | |
| 349 | - def build(call_graph, %IR.AtomType{value: value}, %Context{ |
| 350 | - from_vertex: from_vertex, |
| 351 | - guard?: false, |
| 352 | - pattern?: false, |
| 353 | - modifiers: %{suppress_edges_to_module_vertices?: false} |
| 354 | - }) do |
| 379 | + def build(call_graph, %IR.AtomType{value: value}, from_vertex) do |
| 355 380 | if Reflection.alias?(value) do |
| 356 381 | add_edge(call_graph, from_vertex, value) |
| 357 382 | end |
| @@ -359,250 +384,41 @@ defmodule Hologram.Compiler.CallGraph do | |
| 359 384 | call_graph |
| 360 385 | end |
| 361 386 | |
| 362 | - def build(call_graph, %IR.AtomType{}, _context), do: call_graph |
| 363 | - |
| 364 | - def build( |
| 365 | - call_graph, |
| 366 | - %IR.Clause{match: match, guards: guards, body: body}, |
| 367 | - context |
| 368 | - ) do |
| 369 | - call_graph |
| 370 | - |> build(match, %{context | pattern?: true}) |
| 371 | - |> build(guards, %{context | guard?: true}) |
| 372 | - |> build(body, %{context | pattern?: false}) |
| 373 | - end |
| 374 | - |
| 375 | - def build( |
| 376 | - call_graph, |
| 377 | - %IR.FunctionClause{params: params, guards: guards, body: body}, |
| 378 | - context |
| 379 | - ) do |
| 380 | - call_graph |
| 381 | - |> build(params, %{context | pattern?: true}) |
| 382 | - |> build(guards, %{context | guard?: true}) |
| 383 | - |> build(body, %{context | pattern?: false}) |
| 384 | - end |
| 385 | - |
| 386 | - # Suppress module vertex edges in __impl__/1 clause body. This function is generated by |
| 387 | - # defimpl and its body contains module atoms (the :for and :protocol values) that are |
| 388 | - # metadata, not real dependencies. Without this, reaching any protocol implementation |
| 389 | - # would pull in the target module's entire function tree via the AtomType handler. |
| 390 | - # MFA edges are not affected. |
| 391 | - def build( |
| 392 | - call_graph, |
| 393 | - %IR.FunctionDefinition{name: :__impl__, arity: 1, clause: clause}, |
| 394 | - %Context{from_vertex: from_vertex} = context |
| 395 | - ) do |
| 396 | - fun_def_vertex = {from_vertex, :__impl__, 1} |
| 397 | - |
| 398 | - new_context = %{ |
| 399 | - context |
| 400 | - | from_vertex: fun_def_vertex, |
| 401 | - modifiers: %{context.modifiers | suppress_edges_to_module_vertices?: true} |
| 402 | - } |
| 403 | - |
| 404 | - call_graph |
| 405 | - |> add_edge(from_vertex, fun_def_vertex) |
| 406 | - |> build(clause, new_context) |
| 407 | - end |
| 408 | - |
| 409 | - # Suppress module vertex edges in __protocol__/1 clause body. This function is generated |
| 410 | - # by defprotocol and its body contains module atoms (consolidated implementations, the |
| 411 | - # protocol module) that are metadata, not real dependencies. Without this, reaching any |
| 412 | - # protocol function would pull in every consolidated implementation module's entire |
| 413 | - # function tree. MFA edges are not affected. |
| 414 | - def build( |
| 415 | - call_graph, |
| 416 | - %IR.FunctionDefinition{name: :__protocol__, arity: 1, clause: clause}, |
| 417 | - %Context{from_vertex: from_vertex} = context |
| 418 | - ) do |
| 419 | - fun_def_vertex = {from_vertex, :__protocol__, 1} |
| 420 | - |
| 421 | - new_context = %{ |
| 422 | - context |
| 423 | - | from_vertex: fun_def_vertex, |
| 424 | - modifiers: %{context.modifiers | suppress_edges_to_module_vertices?: true} |
| 425 | - } |
| 426 | - |
| 427 | - call_graph |
| 428 | - |> add_edge(from_vertex, fun_def_vertex) |
| 429 | - |> build(clause, new_context) |
| 430 | - end |
| 431 | - |
| 432 | - # Suppress module vertex edges in __struct__/0 and __struct__/1 clause bodies. These |
| 433 | - # functions are generated by defstruct and their bodies contain module atoms as default |
| 434 | - # field values in the struct map (e.g. calendar: Calendar.ISO in Date). These are data |
| 435 | - # defaults, not real dependencies. Without this, any struct with a module atom as a default |
| 436 | - # value would pull in that module's entire function tree. MFA edges (Enum.reduce/3, |
| 437 | - # Map.merge/2, etc.) are not affected. |
| 438 | - def build( |
| 439 | - call_graph, |
| 440 | - %IR.FunctionDefinition{name: :__struct__, arity: arity, clause: clause}, |
| 441 | - %Context{from_vertex: from_vertex} = context |
| 442 | - ) |
| 443 | - when arity in [0, 1] do |
| 444 | - fun_def_vertex = {from_vertex, :__struct__, arity} |
| 445 | - |
| 446 | - new_context = %{ |
| 447 | - context |
| 448 | - | from_vertex: fun_def_vertex, |
| 449 | - modifiers: %{context.modifiers | suppress_edges_to_module_vertices?: true} |
| 450 | - } |
| 451 | - |
| 452 | - call_graph |
| 453 | - |> add_edge(from_vertex, fun_def_vertex) |
| 454 | - |> build(clause, new_context) |
| 455 | - end |
| 456 | - |
| 457 | - # Suppress module vertex edges in impl_for/1 clause body. This function is generated by |
| 458 | - # defprotocol and its body contains module atoms for ALL implementation modules (return |
| 459 | - # values for type-based dispatch). Without this, every implementation module vertex would |
| 460 | - # be reached, pulling in their entire function trees. MFA edges (struct_impl_for/1 local |
| 461 | - # call in the struct dispatch clause) are not affected. The actual protocol dispatch is |
| 462 | - # handled by add_protocol_call_graph_edges/2. |
| 463 | - def build( |
| 464 | - call_graph, |
| 465 | - %IR.FunctionDefinition{name: :impl_for, arity: 1, clause: clause}, |
| 466 | - %Context{from_vertex: from_vertex} = context |
| 467 | - ) do |
| 468 | - fun_def_vertex = {from_vertex, :impl_for, 1} |
| 469 | - |
| 470 | - new_context = %{ |
| 471 | - context |
| 472 | - | from_vertex: fun_def_vertex, |
| 473 | - modifiers: %{context.modifiers | suppress_edges_to_module_vertices?: true} |
| 474 | - } |
| 475 | - |
| 476 | - call_graph |
| 477 | - |> add_edge(from_vertex, fun_def_vertex) |
| 478 | - |> build(clause, new_context) |
| 479 | - end |
| 480 | - |
| 481 | - # Suppress module vertex edges in impl_for!/1 clause body. This function is generated by |
| 482 | - # defprotocol and its body contains a self-referential module atom (__MODULE__ in the |
| 483 | - # Protocol.UndefinedError message). Without this, reaching impl_for!/1 would pull in |
| 484 | - # the protocol module vertex, making ALL protocol functions and ALL their implementations |
| 485 | - # reachable. MFA edges (impl_for/1 local call, :erlang.error/1, etc.) are not affected. |
| 486 | - # The actual protocol dispatch is handled by add_protocol_call_graph_edges/2. |
| 487 | - def build( |
| 488 | - call_graph, |
| 489 | - %IR.FunctionDefinition{name: :impl_for!, arity: 1, clause: clause}, |
| 490 | - %Context{from_vertex: from_vertex} = context |
| 491 | - ) do |
| 492 | - fun_def_vertex = {from_vertex, :impl_for!, 1} |
| 493 | - |
| 494 | - new_context = %{ |
| 495 | - context |
| 496 | - | from_vertex: fun_def_vertex, |
| 497 | - modifiers: %{context.modifiers | suppress_edges_to_module_vertices?: true} |
| 498 | - } |
| 499 | - |
| 500 | - call_graph |
| 501 | - |> add_edge(from_vertex, fun_def_vertex) |
| 502 | - |> build(clause, new_context) |
| 503 | - end |
| 504 | - |
| 505 | - # Suppress module vertex edges in struct_impl_for/1 clause body. This function is generated |
| 506 | - # by defprotocol and its body contains module atoms for ALL struct-based implementation |
| 507 | - # modules (both as pattern matches and return values). Without this, every struct |
| 508 | - # implementation module vertex would be reached, pulling in their entire function trees. |
| 509 | - # MFA edges (Module.concat/2, Code.ensure_compiled/1 in non-consolidated protocols) are |
| 510 | - # not affected. The actual protocol dispatch is handled by add_protocol_call_graph_edges/2. |
| 511 | - def build( |
| 512 | - call_graph, |
| 513 | - %IR.FunctionDefinition{name: :struct_impl_for, arity: 1, clause: clause}, |
| 514 | - %Context{from_vertex: from_vertex} = context |
| 515 | - ) do |
| 516 | - fun_def_vertex = {from_vertex, :struct_impl_for, 1} |
| 517 | - |
| 518 | - new_context = %{ |
| 519 | - context |
| 520 | - | from_vertex: fun_def_vertex, |
| 521 | - modifiers: %{context.modifiers | suppress_edges_to_module_vertices?: true} |
| 522 | - } |
| 523 | - |
| 524 | - call_graph |
| 525 | - |> add_edge(from_vertex, fun_def_vertex) |
| 526 | - |> build(clause, new_context) |
| 527 | - end |
| 528 | - |
| 529 | - # Suppress module vertex edges in count/1, member?/2, and slice/1 clause bodies in |
| 530 | - # Enumerable protocol implementations. The Enumerable protocol convention requires these |
| 531 | - # functions to return {:error, __MODULE__} when deferring to the default implementation. |
| 532 | - # The __MODULE__ atom creates a module vertex edge that pulls in all sibling functions |
| 533 | - # (including reduce/3 which can cascade heavily). MFA edges are not affected. The actual |
| 534 | - # protocol dispatch is already handled by add_protocol_call_graph_edges/2. |
| 535 387 | def build( |
| 536 388 | call_graph, |
| 537 389 | %IR.FunctionDefinition{name: name, arity: arity, clause: clause}, |
| 538 | - %Context{from_vertex: from_vertex} = context |
| 539 | - ) |
| 540 | - when (name == :count and arity == 1) or |
| 541 | - (name == :member? and arity == 2) or |
| 542 | - (name == :slice and arity == 1) do |
| 543 | - fun_def_vertex = {from_vertex, name, arity} |
| 544 | - call_graph = add_edge(call_graph, from_vertex, fun_def_vertex) |
| 545 | - |
| 546 | - new_context = |
| 547 | - if context.protocol_impl == Enumerable do |
| 548 | - %{ |
| 549 | - context |
| 550 | - | from_vertex: fun_def_vertex, |
| 551 | - modifiers: %{context.modifiers | suppress_edges_to_module_vertices?: true} |
| 552 | - } |
| 553 | - else |
| 554 | - %{context | from_vertex: fun_def_vertex} |
| 555 | - end |
| 556 | - |
| 557 | - build(call_graph, clause, new_context) |
| 558 | - end |
| 559 | - |
| 560 | - def build( |
| 561 | - call_graph, |
| 562 | - %IR.FunctionDefinition{name: name, arity: arity, clause: clause}, |
| 563 | - %Context{from_vertex: from_vertex} = context |
| 390 | + from_vertex |
| 564 391 | ) do |
| 565 392 | fun_def_vertex = {from_vertex, name, arity} |
| 566 393 | |
| 567 394 | call_graph |
| 568 | - |> add_edge(from_vertex, fun_def_vertex) |
| 569 | - |> build(clause, %{context | from_vertex: fun_def_vertex}) |
| 395 | + |> add_vertex(fun_def_vertex) |
| 396 | + |> build(clause, fun_def_vertex) |
| 570 397 | end |
| 571 398 | |
| 572 399 | def build( |
| 573 400 | call_graph, |
| 574 401 | %IR.LocalFunctionCall{function: function, args: args}, |
| 575 | - %Context{from_vertex: {module, _function, _arity}} = context |
| 402 | + {module, _function, _arity} = from_vertex |
| 576 403 | ) do |
| 577 404 | to_vertex = {module, function, Enum.count(args)} |
| 578 405 | |
| 579 406 | call_graph |
| 580 | - |> add_edge(context.from_vertex, to_vertex) |
| 581 | - |> build(args, context) |
| 582 | - end |
| 583 | - |
| 584 | - def build(call_graph, %IR.MatchOperator{left: left, right: right}, context) do |
| 585 | - call_graph |
| 586 | - |> build(left, %{context | pattern?: true}) |
| 587 | - |> build(right, context) |
| 407 | + |> add_edge(from_vertex, to_vertex) |
| 408 | + |> build(args, from_vertex) |
| 588 409 | end |
| 589 410 | |
| 590 411 | def build( |
| 591 412 | call_graph, |
| 592 413 | %IR.ModuleDefinition{module: %IR.AtomType{value: module}, body: body}, |
| 593 | - context |
| 414 | + _from_vertex |
| 594 415 | ) do |
| 595 | - new_context = %{ |
| 596 | - context |
| 597 | - | from_vertex: module, |
| 598 | - protocol_impl: Reflection.protocol_impl(module) |
| 599 | - } |
| 600 | - |
| 601 416 | call_graph |
| 417 | + |> maybe_add_templatable_call_graph_edges(module) |
| 602 418 | |> maybe_add_protocol_call_graph_edges(module) |
| 603 419 | |> maybe_add_struct_call_graph_edges(module) |
| 604 420 | |> maybe_add_ecto_schema_call_graph_edges(module) |
| 605 | - |> build(body, new_context) |
| 421 | + |> build(body, module) |
| 606 422 | end |
| 607 423 | |
| 608 424 | # :erlang.apply/3 is not added to the call graph because the encoder |
| @@ -618,12 +434,12 @@ defmodule Hologram.Compiler.CallGraph do | |
| 618 434 | %IR.ListType{data: args} |
| 619 435 | ] |
| 620 436 | }, |
| 621 | - context |
| 437 | + from_vertex |
| 622 438 | ) do |
| 623 439 | to_vertex = {module, function, Enum.count(args)} |
| 624 | - add_edge(call_graph, context.from_vertex, to_vertex) |
| 440 | + add_edge(call_graph, from_vertex, to_vertex) |
| 625 441 | |
| 626 | - build(call_graph, args, context) |
| 442 | + build(call_graph, args, from_vertex) |
| 627 443 | end |
| 628 444 | |
| 629 445 | # :erlang.apply/3 is not added to the call graph because the encoder |
| @@ -635,153 +451,12 @@ defmodule Hologram.Compiler.CallGraph do | |
| 635 451 | function: :apply, |
| 636 452 | args: [module, function, %IR.ListType{data: args}] |
| 637 453 | }, |
| 638 | - context |
| 454 | + from_vertex |
| 639 455 | ) do |
| 640 456 | call_graph |
| 641 | - |> build(module, context) |
| 642 | - |> build(function, context) |
| 643 | - |> build(args, context) |
| 644 | - end |
| 645 | - |
| 646 | - # Suppress module vertex edges in the third argument (error options) of :erlang.error/3. |
| 647 | - # The Elixir compiler transforms `raise` into :erlang.error/3 with |
| 648 | - # error_info: %{module: Exception} as the third argument. This Exception |
| 649 | - # module atom is compiler-injected metadata for error formatting, not a |
| 650 | - # real dependency. Without this, every `raise` would pull in the Exception |
| 651 | - # module's entire function tree (58 functions). MFA edges are not affected. |
| 652 | - def build( |
| 653 | - call_graph, |
| 654 | - %IR.RemoteFunctionCall{ |
| 655 | - module: %IR.AtomType{value: :erlang}, |
| 656 | - function: :error, |
| 657 | - args: [reason, args, error_options] |
| 658 | - }, |
| 659 | - context |
| 660 | - ) do |
| 661 | - add_edge(call_graph, context.from_vertex, {:erlang, :error, 3}) |
| 662 | - |
| 663 | - suppressed_context = %{ |
| 664 | - context |
| 665 | - | modifiers: %{context.modifiers | suppress_edges_to_module_vertices?: true} |
| 666 | - } |
| 667 | - |
| 668 | - call_graph |
| 669 | - |> build(reason, context) |
| 670 | - |> build(args, context) |
| 671 | - |> build(error_options, suppressed_context) |
| 672 | - end |
| 673 | - |
| 674 | - # Skip module atoms in the first argument (deduplication key) of IO.warn_once/3. |
| 675 | - # The key is typically a tuple like {ModuleName, some_value} used to track |
| 676 | - # whether a warning has already been printed. The module atom in the key is |
| 677 | - # a namespace identifier, not a real dependency. Without this, any module |
| 678 | - # calling IO.warn_once with its own name as a key would pull in its entire |
| 679 | - # function tree via the module vertex. |
| 680 | - # Non-atom elements in the key are still traversed to capture any function call edges. |
| 681 | - def build( |
| 682 | - call_graph, |
| 683 | - %IR.RemoteFunctionCall{ |
| 684 | - module: %IR.AtomType{value: IO}, |
| 685 | - function: :warn_once, |
| 686 | - args: [key, message, stacktrace_depth] |
| 687 | - }, |
| 688 | - context |
| 689 | - ) do |
| 690 | - add_edge(call_graph, context.from_vertex, {IO, :warn_once, 3}) |
| 691 | - |
| 692 | - suppressed_context = %{ |
| 693 | - context |
| 694 | - | modifiers: %{context.modifiers | suppress_edges_to_module_vertices?: true} |
| 695 | - } |
| 696 | - |
| 697 | - call_graph |
| 698 | - |> build(key, suppressed_context) |
| 699 | - |> build(message, context) |
| 700 | - |> build(stacktrace_depth, context) |
| 701 | - end |
| 702 | - |
| 703 | - # Suppress module vertex edges in Kernel.inspect/1,2 first argument. Module atoms passed |
| 704 | - # to inspect are used for string formatting (e.g. in error messages), not as real |
| 705 | - # dependencies. Without this, code like `inspect(MyModule)` in error paths would pull in |
| 706 | - # MyModule's entire function tree. MFA edges are not affected. The second argument |
| 707 | - # (options) is traversed normally. |
| 708 | - def build( |
| 709 | - call_graph, |
| 710 | - %IR.RemoteFunctionCall{ |
| 711 | - module: %IR.AtomType{value: Kernel}, |
| 712 | - function: :inspect, |
| 713 | - args: [term | opts] |
| 714 | - }, |
| 715 | - context |
| 716 | - ) do |
| 717 | - to_vertex = {Kernel, :inspect, 1 + length(opts)} |
| 718 | - add_edge(call_graph, context.from_vertex, to_vertex) |
| 719 | - |
| 720 | - suppressed_context = %{ |
| 721 | - context |
| 722 | - | modifiers: %{context.modifiers | suppress_edges_to_module_vertices?: true} |
| 723 | - } |
| 724 | - |
| 725 | - call_graph |
| 726 | - |> build(term, suppressed_context) |
| 727 | - |> build(opts, context) |
| 728 | - end |
| 729 | - |
| 730 | - # For Kernel.struct!/2 with a literal module atom as the first argument, create targeted |
| 731 | - # edges to {module, :__struct__, 0} and {module, :__struct__, 1} instead of the module |
| 732 | - # vertex. Kernel.struct!/2 only uses the module to call __struct__/0 and __struct__/1. |
| 733 | - # This is the same approach as the __struct__ key-in-map special case. Without this, |
| 734 | - # every defexception module's exception/1 function (which calls Kernel.struct!(__MODULE__, |
| 735 | - # args)) would pull in the module's entire function tree. |
| 736 | - def build( |
| 737 | - call_graph, |
| 738 | - %IR.RemoteFunctionCall{ |
| 739 | - module: %IR.AtomType{value: Kernel}, |
| 740 | - function: :struct!, |
| 741 | - args: [%IR.AtomType{value: module}, fields] |
| 742 | - }, |
| 743 | - context |
| 744 | - ) do |
| 745 | - call_graph |
| 746 | - |> add_edge(context.from_vertex, {Kernel, :struct!, 2}) |
| 747 | - |> add_edge(context.from_vertex, {module, :__struct__, 0}) |
| 748 | - |> add_edge(context.from_vertex, {module, :__struct__, 1}) |
| 749 | - |> build(fields, context) |
| 750 | - end |
| 751 | - |
| 752 | - # Suppress module vertex edges in the :protocol key value of |
| 753 | - # Protocol.UndefinedError.exception/1 args. This is expanded from: |
| 754 | - # raise Protocol.UndefinedError, protocol: SomeProtocol, value: data |
| 755 | - # The protocol module atom is just metadata identifying the protocol in the error message, |
| 756 | - # not a real dependency. Without this, raising Protocol.UndefinedError in protocol |
| 757 | - # implementation fallback clauses would pull in the protocol module's entire function tree. |
| 758 | - # MFA edges are not affected. Other keyword entries (e.g. :value, :description) are |
| 759 | - # traversed normally. |
| 760 | - def build( |
| 761 | - call_graph, |
| 762 | - %IR.RemoteFunctionCall{ |
| 763 | - module: %IR.AtomType{value: Protocol.UndefinedError}, |
| 764 | - function: :exception, |
| 765 | - args: [%IR.ListType{data: data}] |
| 766 | - }, |
| 767 | - context |
| 768 | - ) do |
| 769 | - add_edge(call_graph, context.from_vertex, {Protocol.UndefinedError, :exception, 1}) |
| 770 | - |
| 771 | - suppressed_context = %{ |
| 772 | - context |
| 773 | - | modifiers: %{context.modifiers | suppress_edges_to_module_vertices?: true} |
| 774 | - } |
| 775 | - |
| 776 | - Enum.each(data, fn |
| 777 | - %IR.TupleType{data: [%IR.AtomType{value: :protocol}, module]} -> |
| 778 | - build(call_graph, module, suppressed_context) |
| 779 | - |
| 780 | - entry -> |
| 781 | - build(call_graph, entry, context) |
| 782 | - end) |
| 783 | - |
| 784 | - call_graph |
| 457 | + |> build(module, from_vertex) |
| 458 | + |> build(function, from_vertex) |
| 459 | + |> build(args, from_vertex) |
| 785 460 | end |
| 786 461 | |
| 787 462 | def build( |
| @@ -791,84 +466,40 @@ defmodule Hologram.Compiler.CallGraph do | |
| 791 466 | function: function, |
| 792 467 | args: args |
| 793 468 | }, |
| 794 | - context |
| 469 | + from_vertex |
| 795 470 | ) do |
| 796 471 | to_vertex = {module, function, Enum.count(args)} |
| 797 | - add_edge(call_graph, context.from_vertex, to_vertex) |
| 472 | + add_edge(call_graph, from_vertex, to_vertex) |
| 798 473 | |
| 799 | - build(call_graph, args, context) |
| 474 | + build(call_graph, args, from_vertex) |
| 800 475 | end |
| 801 476 | |
| 802 | - def build( |
| 803 | - call_graph, |
| 804 | - %IR.TryCatchClause{kind: kind, value: value, guards: guards, body: body}, |
| 805 | - context |
| 806 | - ) do |
| 807 | - call_graph |
| 808 | - |> build(kind, context) |
| 809 | - |> build(value, %{context | pattern?: true}) |
| 810 | - |> build(guards, %{context | guard?: true}) |
| 811 | - |> build(body, context) |
| 812 | - end |
| 813 | - |
| 814 | - # Rescue clause exception modules are used for pattern matching against exception types, |
| 815 | - # not as module dependencies. Without this, rescuing UndefinedFunctionError (as Access.get/3 |
| 816 | - # and Access.fetch/2 do) would pull in the entire UndefinedFunctionError function tree |
| 817 | - # (blame/2, hint/4, exports_for/1, etc.). |
| 818 | - def build( |
| 819 | - call_graph, |
| 820 | - %IR.TryRescueClause{variable: variable, modules: modules, body: body}, |
| 821 | - context |
| 822 | - ) do |
| 823 | - call_graph |
| 824 | - |> build(variable, %{context | pattern?: true}) |
| 825 | - |> build(modules, %{context | pattern?: true}) |
| 826 | - |> build(body, context) |
| 827 | - end |
| 828 | - |
| 829 | - def build(call_graph, list, context) when is_list(list) do |
| 830 | - Enum.each(list, &build(call_graph, &1, context)) |
| 477 | + def build(call_graph, list, from_vertex) when is_list(list) do |
| 478 | + Enum.each(list, &build(call_graph, &1, from_vertex)) |
| 831 479 | call_graph |
| 832 480 | end |
| 833 481 | |
| 834 | - def build(call_graph, map, context) when is_map(map) do |
| 482 | + def build(call_graph, map, from_vertex) when is_map(map) do |
| 835 483 | map |
| 836 484 | |> Map.to_list() |
| 837 485 | |> Enum.each(fn {key, value} -> |
| 838 | - build(call_graph, key, context) |
| 839 | - build(call_graph, value, context) |
| 486 | + call_graph |
| 487 | + |> build(key, from_vertex) |
| 488 | + |> build(value, from_vertex) |
| 840 489 | end) |
| 841 490 | |
| 842 491 | call_graph |
| 843 492 | end |
| 844 493 | |
| 845 | - # For __struct__ key in map data, create targeted edges to __struct__/0 and __struct__/1 |
| 846 | - # instead of to the module vertex, to avoid pulling in the module's entire function tree. |
| 847 | - def build(call_graph, {%IR.AtomType{value: :__struct__}, %IR.AtomType{value: module}}, %Context{ |
| 848 | - from_vertex: from_vertex, |
| 849 | - guard?: false, |
| 850 | - pattern?: false |
| 851 | - }) do |
| 852 | - if Reflection.alias?(module) do |
| 853 | - call_graph |
| 854 | - |> add_edge(from_vertex, {module, :__struct__, 0}) |
| 855 | - |> add_edge(from_vertex, {module, :__struct__, 1}) |
| 856 | - else |
| 857 | - call_graph |
| 858 | - end |
| 859 | - end |
| 860 | - |
| 861 | - def build(call_graph, {%IR.AtomType{value: :__struct__}, _value}, _context), do: call_graph |
| 862 | - |
| 863 | - def build(call_graph, tuple, context) when is_tuple(tuple) do |
| 494 | + def build(call_graph, tuple, from_vertex) when is_tuple(tuple) do |
| 864 495 | tuple |
| 865 496 | |> Tuple.to_list() |
| 866 | - |> Enum.each(&build(call_graph, &1, context)) |
| 497 | + |> Enum.each(&build(call_graph, &1, from_vertex)) |
| 867 498 | |
| 868 499 | call_graph |
| 869 500 | end |
| 870 501 | |
| 871 | - def build(call_graph, _ir, _context), do: call_graph |
| 502 | + def build(call_graph, _ir, _from_vertex), do: call_graph |
| 872 503 | |
| 873 504 | @doc """ |
| 874 505 | Builds a call graph from a module definition IR located in the given IR PLT. |
| @@ -876,7 +507,7 @@ defmodule Hologram.Compiler.CallGraph do | |
| 876 507 | @spec build_for_module(t, PLT.t(), module) :: t |
| 877 508 | def build_for_module(call_graph, ir_plt, module) do |
| 878 509 | module_def = PLT.get!(ir_plt, module) |
| 879 | - build(call_graph, module_def, %Context{}) |
| 510 | + build(call_graph, module_def) |
| 880 511 | end |
| 881 512 | |
| 882 513 | @doc """ |
| @@ -890,32 +521,6 @@ defmodule Hologram.Compiler.CallGraph do | |
| 890 521 | start(graph) |
| 891 522 | end |
| 892 523 | |
| 893 | - @doc """ |
| 894 | - Computes cascade entries for module vertices in a call graph. |
| 895 | - |
| 896 | - Returns a list of `{source, module, downstream_mfa_count}` tuples sorted by |
| 897 | - downstream MFA count (biggest cascades first). |
| 898 | - """ |
| 899 | - @spec compute_cascades(Digraph.t(), MapSet.t(), MapSet.t()) :: [ |
| 900 | - {vertex, vertex, non_neg_integer} |
| 901 | - ] |
| 902 | - def compute_cascades(graph, module_vertices, reachable) do |
| 903 | - module_vertices |
| 904 | - |> Enum.flat_map(fn module -> |
| 905 | - downstream_mfa_count = |
| 906 | - graph |
| 907 | - |> Digraph.reachable([module]) |
| 908 | - |> Enum.count(&match?({_module, _function, _arity}, &1)) |
| 909 | - |
| 910 | - graph |
| 911 | - |> Digraph.incoming_edges(module) |
| 912 | - |> Enum.map(&elem(&1, 0)) |
| 913 | - |> Enum.filter(&MapSet.member?(reachable, &1)) |
| 914 | - |> Enum.map(fn source -> {source, module, downstream_mfa_count} end) |
| 915 | - end) |
| 916 | - |> Enum.sort_by(fn {_source, _module, count} -> count end, :desc) |
| 917 | - end |
| 918 | - |
| 919 524 | @doc """ |
| 920 525 | Serializes the call graph and writes it to a file. |
| 921 526 | |
| @@ -953,18 +558,6 @@ defmodule Hologram.Compiler.CallGraph do | |
| 953 558 | Agent.get(pid, & &1, :infinity) |
| 954 559 | end |
| 955 560 | |
| 956 | - @doc """ |
| 957 | - Returns the underlying digraph pruned to the given page, with server-only |
| 958 | - and other-page MFAs removed. |
| 959 | - """ |
| 960 | - @spec get_pruned_page_graph(t, module) :: Digraph.t() |
| 961 | - def get_pruned_page_graph(call_graph, page_module) do |
| 962 | - call_graph |
| 963 | - |> get_graph() |
| 964 | - |> remove_server_only_mfas() |
| 965 | - |> remove_other_page_mfas(page_module) |
| 966 | - end |
| 967 | - |
| 968 561 | @doc """ |
| 969 562 | Checks if an edge exists between two given vertices in the call graph. |
| 970 563 | """ |
| @@ -992,7 +585,7 @@ defmodule Hologram.Compiler.CallGraph do | |
| 992 585 | graph = get_graph(call_graph) |
| 993 586 | |
| 994 587 | graph |
| 995 | - |> Digraph.reaching([{Task, :await, 1}], skip_module_vertices: true) |
| 588 | + |> Digraph.reaching([{Task, :await, 1}], opaque_vertex?: &is_atom/1) |
| 996 589 | # Excludes bare module atom vertices, keeping only MFA tuples. |
| 997 590 | # No Reflection.module?/1 guard needed in the filter (unlike reachable_mfas/2) because |
| 998 591 | # the result is only used for MapSet.member? lookups against already-included MFAs. |
| @@ -1000,16 +593,48 @@ defmodule Hologram.Compiler.CallGraph do | |
| 1000 593 | |> MapSet.new() |
| 1001 594 | end |
| 1002 595 | |
| 596 | + @doc """ |
| 597 | + Lists the entry MFAs {module, function, arity} for a given page module. |
| 598 | + |
| 599 | + This function returns a list of MFAs that are considered entry points for a page, |
| 600 | + including functions from both the page module and its associated layout module. |
| 601 | + |
| 602 | + ## Parameters |
| 603 | + |
| 604 | + * `page_module` - The module of the page for which to list entry MFAs. |
| 605 | + |
| 606 | + ## Returns |
| 607 | + |
| 608 | + A list of MFAs (tuples of {module, function, arity}) that serve as entry points |
| 609 | + for the given page module and its layout. |
| 610 | + """ |
| 611 | + @spec list_page_entry_mfas(module) :: [mfa] |
| 612 | + def list_page_entry_mfas(page_module) do |
| 613 | + layout_module = page_module.__layout_module__() |
| 614 | + |
| 615 | + [ |
| 616 | + {page_module, :__layout_module__, 0}, |
| 617 | + {page_module, :__layout_props__, 0}, |
| 618 | + {page_module, :__params__, 0}, |
| 619 | + {page_module, :__route__, 0}, |
| 620 | + {page_module, :action, 3}, |
| 621 | + {page_module, :template, 0}, |
| 622 | + {layout_module, :__props__, 0}, |
| 623 | + {layout_module, :action, 3}, |
| 624 | + {layout_module, :template, 0} |
| 625 | + ] |
| 626 | + end |
| 627 | + |
| 1003 628 | @doc """ |
| 1004 629 | Returns the sorted list of MFAs that are reachable by the given page. |
| 1005 630 | """ |
| 1006 631 | @spec list_page_mfas(t, module) :: [mfa] |
| 1007 632 | def list_page_mfas(call_graph, page_module) do |
| 633 | + entry_mfas = list_page_entry_mfas(page_module) |
| 1008 634 | graph = get_graph(call_graph) |
| 1009 635 | |
| 1010 | - call_graph |
| 1011 | - |> get_pruned_page_graph(page_module) |
| 1012 | - |> sorted_reachable_mfas([page_module]) |
| 636 | + graph |
| 637 | + |> sorted_reachable_mfas(entry_mfas) |
| 1013 638 | |> reject_hex_mfas() |
| 1014 639 | |> add_reflection_mfas_reachable_from_server_inits(page_module, graph) |
| 1015 640 | |> Enum.uniq() |
| @@ -1024,12 +649,9 @@ defmodule Hologram.Compiler.CallGraph do | |
| 1024 649 | @spec list_runtime_entry_mfas :: [mfa] |
| 1025 650 | def list_runtime_entry_mfas do |
| 1026 651 | @mfas_used_by_client_runtime |
| 1027 | - |> Enum.reduce( |
| 1028 | - @mfas_used_by_all_pages_and_components, |
| 1029 | - fn {_key, mfas}, acc -> |
| 1030 | - mfas ++ acc |
| 1031 | - end |
| 1032 | - ) |
| 652 | + |> Enum.reduce(@mfas_used_by_all_pages_and_components, fn {_key, mfas}, acc -> |
| 653 | + mfas ++ acc |
| 654 | + end) |
| 1033 655 | |> Enum.uniq() |
| 1034 656 | |> Enum.sort() |
| 1035 657 | end |
| @@ -1045,7 +667,6 @@ defmodule Hologram.Compiler.CallGraph do | |
| 1045 667 | |
| 1046 668 | call_graph |
| 1047 669 | |> get_graph() |
| 1048 | - |> add_edges_for_erlang_functions() |
| 1049 670 | |> sorted_reachable_mfas(entry_mfas) |
| 1050 671 | |> reject_hex_mfas() |
| 1051 672 | end |
| @@ -1309,9 +930,24 @@ defmodule Hologram.Compiler.CallGraph do | |
| 1309 930 | Agent.get(pid, &Digraph.vertices/1, :infinity) |
| 1310 931 | end |
| 1311 932 | |
| 1312 | - # Add call graph edges for Erlang functions depending on other Erlang functions. |
| 1313 | - defp add_edges_for_erlang_functions(graph) do |
| 1314 | - Digraph.add_edges(graph, @erlang_mfa_edges) |
| 933 | + # TODO: think how to avoid this |
| 934 | + # A component module can be passed as a prop to another component, allowing dynamic usage. |
| 935 | + # In such cases, when this scenario is identified, it becomes necessary |
| 936 | + # to include the entire component on the client side. |
| 937 | + # This is because we lack precise information about which specific component functions will be used. |
| 938 | + defp add_component_call_graph_edges(call_graph, module) do |
| 939 | + call_graph |
| 940 | + |> add_edge(module, {module, :__props__, 0}) |
| 941 | + |> add_edge(module, {module, :action, 3}) |
| 942 | + |> add_edge(module, {module, :init, 2}) |
| 943 | + |> add_edge(module, {module, :template, 0}) |
| 944 | + end |
| 945 | + |
| 946 | + # __props__/0 and __route__/0 functions are needed to build page link href (e.g. in Hologram.UI.Link component). |
| 947 | + defp add_page_call_graph_edges(call_graph, module) do |
| 948 | + call_graph |
| 949 | + |> add_edge(module, {module, :__params__, 0}) |
| 950 | + |> add_edge(module, {module, :__route__, 0}) |
| 1315 951 | end |
| 1316 952 | |
| 1317 953 | defp add_protocol_call_graph_edges(call_graph, module) do |
| @@ -1406,6 +1042,18 @@ defmodule Hologram.Compiler.CallGraph do | |
| 1406 1042 | call_graph |
| 1407 1043 | end |
| 1408 1044 | |
| 1045 | + defp maybe_add_templatable_call_graph_edges(call_graph, module) do |
| 1046 | + if Reflection.page?(module) do |
| 1047 | + add_page_call_graph_edges(call_graph, module) |
| 1048 | + end |
| 1049 | + |
| 1050 | + if Reflection.component?(module) do |
| 1051 | + add_component_call_graph_edges(call_graph, module) |
| 1052 | + end |
| 1053 | + |
| 1054 | + call_graph |
| 1055 | + end |
| 1056 | + |
| 1409 1057 | # When modules that are protocol implementations are added or edited, the protocol |
| 1410 1058 | # module itself (e.g. Enumerable) is unchanged and not re-processed by patch. Its |
| 1411 1059 | # dispatch edges remain stale. This function re-runs add_protocol_call_graph_edges |
| @@ -1423,38 +1071,4 @@ defmodule Hologram.Compiler.CallGraph do | |
| 1423 1071 | defp remove_module_vertices(call_graph, module) do |
| 1424 1072 | remove_vertices(call_graph, module_vertices(call_graph, module)) |
| 1425 1073 | end |
| 1426 | - |
| 1427 | - defp remove_other_page_mfas(graph, page_module) do |
| 1428 | - other_page_modules = |
| 1429 | - Reflection.list_pages() |
| 1430 | - |> Enum.reject(&(&1 == page_module)) |
| 1431 | - |> MapSet.new() |
| 1432 | - |
| 1433 | - other_page_vertices = |
| 1434 | - graph |
| 1435 | - |> Digraph.vertices() |
| 1436 | - |> Enum.filter(fn |
| 1437 | - {module, fun, arity} -> |
| 1438 | - MapSet.member?(other_page_modules, module) && |
| 1439 | - {fun, arity} not in @cross_page_funs |
| 1440 | - |
| 1441 | - _fallback -> |
| 1442 | - false |
| 1443 | - end) |
| 1444 | - |
| 1445 | - Digraph.remove_vertices(graph, other_page_vertices) |
| 1446 | - end |
| 1447 | - |
| 1448 | - defp remove_server_only_mfas(graph) do |
| 1449 | - server_only_vertices = |
| 1450 | - graph |
| 1451 | - |> Digraph.vertices() |
| 1452 | - |> Enum.filter(fn |
| 1453 | - {module, :command, 3} -> Reflection.templatable?(module) |
| 1454 | - {module, :init, 3} -> Reflection.templatable?(module) |
| 1455 | - _fallback -> false |
| 1456 | - end) |
| 1457 | - |
| 1458 | - Digraph.remove_vertices(graph, server_only_vertices) |
| 1459 | - end |
| 1460 1074 | end |
| @@ -205,11 +205,13 @@ defmodule Hologram.Compiler.Digraph do | |
| 205 205 | Non-existent target vertices are ignored. |
| 206 206 | |
| 207 207 | ## Options |
| 208 | + * `:opaque_vertex?` - a one-arity function that receives a vertex and returns a boolean. |
| 209 | + When it returns `true`, the vertex is included in the result but its incoming edges |
| 210 | + are not traversed. Defaults to `nil`. |
| 208 211 | |
| 209 | - * `:skip_module_vertices` - when `true`, module vertices (atoms) are added to the |
| 210 | - result but their incoming edges are not traversed. This prevents module atom |
| 211 | - references from propagating through module-to-function-definition edges. |
| 212 | - Defaults to `false`. |
| 212 | + * `:opaque_vertices` - a `MapSet` of vertices whose incoming edges should not be |
| 213 | + traversed. The vertices themselves are still included in the result when reached, |
| 214 | + but the BFS does not follow their incoming edges. Defaults to `nil`. |
| 213 215 | """ |
| 214 216 | @spec reaching(t, [vertex], keyword) :: [vertex] |
| 215 217 | def reaching(graph, target_vertices, opts \\ []) do |
| @@ -449,12 +451,19 @@ defmodule Hologram.Compiler.Digraph do | |
| 449 451 | end |
| 450 452 | |
| 451 453 | # BFS traversal for reachable vertices |
| 452 | - # credo:disable-for-lines:31 Credo.Check.Refactor.Nesting |
| 454 | + # credo:disable-for-lines:37 Credo.Check.Refactor.Nesting |
| 453 455 | # The above Credo check is disabled because the function is optimised this way |
| 454 456 | defp bfs_reachable(queue, visited, edges, opts) do |
| 455 457 | case :queue.out(queue) do |
| 456 458 | {{:value, current}, rest_queue} -> |
| 457 | - if opts[:skip_module_vertices] && is_atom(current) do |
| 459 | + opaque_vertex? = opts[:opaque_vertex?] |
| 460 | + opaque_vertices = opts[:opaque_vertices] |
| 461 | + |
| 462 | + skip? = |
| 463 | + (opaque_vertex? && opaque_vertex?.(current)) || |
| 464 | + (opaque_vertices && MapSet.member?(opaque_vertices, current)) |
| 465 | + |
| 466 | + if skip? do |
| 458 467 | bfs_reachable(rest_queue, visited, edges, opts) |
| 459 468 | else |
| 460 469 | # Get neighbors of current vertex |
Loading more files…