Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for HEEx compilation #323

Merged
merged 6 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule Lexical.RemoteControl.Build.Document do
alias Lexical.Document
alias Lexical.RemoteControl.Build.Document.Compilers

@compilers [Compilers.Config, Compilers.Elixir, Compilers.EEx, Compilers.NoOp]
@compilers [Compilers.Config, Compilers.Elixir, Compilers.EEx, Compilers.HEEx, Compilers.NoOp]

def compile(%Document{} = document) do
compiler = Enum.find(@compilers, & &1.recognizes?(document))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule Lexical.RemoteControl.Build.Document.Compilers.EEx do
"""
alias Lexical.Document
alias Lexical.Plugin.V1.Diagnostic.Result
alias Lexical.RemoteControl.Build
alias Lexical.RemoteControl.Build.Document.Compiler
alias Lexical.RemoteControl.Build.Document.Compilers

Expand All @@ -18,8 +19,15 @@ defmodule Lexical.RemoteControl.Build.Document.Compilers.EEx do
end

def compile(%Document{} = document) do
with {:ok, quoted} <- eex_to_quoted(document) do
Compilers.Quoted.compile(document, quoted, "EEx")
with {:ok, quoted} <- eex_to_quoted(document),
:ok <- eval_quoted(document, quoted) do
compile_quoted(document, quoted)
end
end

defp compile_quoted(%Document{} = document, quoted) do
with {:error, errors} <- Compilers.Quoted.compile(document, quoted, "EEx") do
{:error, reject_undefined_assigns(errors)}
end
end

Expand All @@ -37,6 +45,72 @@ defmodule Lexical.RemoteControl.Build.Document.Compilers.EEx do
end
end

defp eval_quoted(%Document{} = document, quoted_ast) do
result =
if Elixir.Features.with_diagnostics?() do
eval_quoted_with_diagnostics(quoted_ast, document.path)
else
do_eval_quoted(quoted_ast, document.path)
end

case result do
{:ok, _eval_result} ->
:ok

{{:ok, _eval_result}, _} ->
# Ignore warnings for now
# because they will be handled by `compile_quoted/2`
# like: `assign @thing not available in EEx template`
:ok

{:exception, exception, stack, _quoted_ast} ->
converted =
document
|> Build.Error.error_to_diagnostic(exception, stack, quoted_ast)
|> Map.put(:source, "EEx")

{:error, [converted]}

{{:exception, exception, stack, _quoted_ast}, all_errors_and_warnings} ->
converted = Build.Error.error_to_diagnostic(document, exception, stack, quoted_ast)
maybe_diagnostics = Build.Error.diagnostics_from_mix(document, all_errors_and_warnings)

diagnostics =
[converted | maybe_diagnostics]
|> Enum.reverse()
|> Build.Error.refine_diagnostics()
|> Enum.map(&Map.replace!(&1, :source, "EEx"))

{:error, diagnostics}
end
end

defp eval_quoted_with_diagnostics(quoted_ast, path) do
# Using apply to prevent a compile warning on elixir < 1.15
# credo:disable-for-next-line
apply(Code, :with_diagnostics, [fn -> do_eval_quoted(quoted_ast, path) end])
end

def do_eval_quoted(quoted_ast, path) do
try do
{result, _} = Code.eval_quoted(quoted_ast, [assigns: %{}], file: path)
{:ok, result}
rescue
exception ->
{filled_exception, stack} = Exception.blame(:error, exception, __STACKTRACE__)
{:exception, filled_exception, stack, quoted_ast}
end
end

defp reject_undefined_assigns(errors) do
# NOTE: Ignoring error for assigns makes sense,
# because we don't want such a error report,
# for example: `<%= @name %>`
Enum.reject(errors, fn %Result{message: message} ->
message =~ ~s[undefined variable "assigns"]
end)
end

defp error_to_result(%Document{} = document, %EEx.SyntaxError{} = error) do
position = {error.line, error.column}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
defmodule Lexical.RemoteControl.Build.Document.Compilers.HEEx do
@moduledoc """
A compiler for .heex files
"""
alias Lexical.Document
alias Lexical.Plugin.V1.Diagnostic.Result
alias Lexical.RemoteControl.Build.Document.Compiler
alias Lexical.RemoteControl.Build.Document.Compilers
require Logger

@behaviour Compiler

def recognizes?(%Document{} = document) do
Path.extname(document.path) == ".heex"
end

def enabled? do
true
end

def compile(%Document{} = document) do
case heex_to_quoted(document) do
{:ok, _} ->
Compilers.EEx.compile(document)

other ->
other
end
end

defp heex_to_quoted(%Document{} = document) do
try do
source = Document.to_string(document)

opts =
[
source: source,
file: document.path,
caller: __ENV__,
engine: Phoenix.LiveView.TagEngine,
subengine: Phoenix.LiveView.Engine,
tag_handler: Phoenix.LiveView.HTMLEngine
]

quoted = EEx.compile_string(source, opts)

{:ok, quoted}
rescue
error ->
{:error, [error_to_result(document, error)]}
end
end

defp error_to_result(%Document{} = document, %EEx.SyntaxError{} = error) do
position = {error.line, error.column}

Result.new(document.uri, position, error.message, :error, "EEx")
end

defp error_to_result(document, %error_struct{} = error)
when error_struct in [
TokenMissingError,
Phoenix.LiveView.Tokenizer.ParseError
] do
position = {error.line, error.column}
Result.new(document.uri, position, error.description, :error, "HEEx")
end
end
3 changes: 2 additions & 1 deletion apps/remote_control/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ defmodule Lexical.RemoteControl.MixProject do
{:lexical_test, path: "../../projects/lexical_test", only: :test},
{:patch, "~> 0.12", only: [:dev, :test], optional: true, runtime: false},
{:path_glob, "~> 0.2", optional: true},
{:sourceror, "~> 0.14.0"}
{:sourceror, "~> 0.14.0"},
{:phoenix_live_view, "~> 0.19.5", only: [:test], optional: true, runtime: false}
]
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ defmodule Lexical.RemoteControl.Build.Document.Compilers.EExTest do
end
end

describe "compile/1" do
describe "eex_to_quoted/1" do
setup [:with_capture_server]

test "handles syntax errors" do
Expand All @@ -65,6 +65,14 @@ defmodule Lexical.RemoteControl.Build.Document.Compilers.EExTest do
assert result.source == "EEx"
assert result.uri
end
end

describe "compile_quoted/2" do
setup [:with_capture_server]

setup do
Code.compiler_options(parser_options: [columns: true, token_metadata: true])
end

test "handles unused variables" do
assert {:ok, [%Result{} = result]} =
Expand All @@ -75,10 +83,45 @@ defmodule Lexical.RemoteControl.Build.Document.Compilers.EExTest do
|> compile()

assert result.message =~ ~s["something" is unused]
assert result.position == 1
assert result.position in [1, {1, 5}]
assert result.severity == :warning
assert result.source == "EEx"
assert result.uri =~ "file:///file.eex"
end
end

describe "eval_quoted/2" do
test "handles undefinied function" do
document = document_with_content(~q[
<%= IO.uts("thing") %>
])

assert {:error, [%Result{} = result]} = compile(document)
assert result.message =~ "function IO.uts/1 is undefined or private"
assert result.position == {1, 8}
assert result.severity == :error
assert result.source == "EEx"
assert result.uri =~ "file:///file.eex"
end

@tag :with_diagnostics
test "handles undefinied variable" do
document = document_with_content(~q[
<%= thing %>
])

assert {:error, [%Result{} = result]} = compile(document)

if Features.with_diagnostics?() do
assert result.message =~ "undefined variable \"thing\""
else
assert result.message =~ "undefined function thing/0"
end

assert result.position in [1, {1, 5}]
assert result.severity == :error
assert result.source == "EEx"
assert result.uri =~ "file:///file.eex"
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
defmodule Lexical.RemoteControl.Build.Document.Compilers.HeexTest do
alias Lexical.Document
alias Lexical.Plugin.V1.Diagnostic.Result
alias Lexical.RemoteControl.Build.CaptureServer
alias Lexical.RemoteControl.Build.Document.Compilers
alias Lexical.RemoteControl.Dispatch
alias Lexical.RemoteControl.ModuleMappings

import Lexical.Test.CodeSigil
import Compilers.HEEx, only: [compile: 1]

use ExUnit.Case

def with_capture_server(_) do
start_supervised!(CaptureServer)
start_supervised!(Dispatch)
start_supervised!(ModuleMappings)
:ok
end

def document_with_content(content) do
Document.new("file:///file.heex", content, 0)
end

setup do
Code.compiler_options(parser_options: [columns: true, token_metadata: true])
end

describe "compile/1" do
setup [:with_capture_server]

test "handles valid HEEx content" do
document = document_with_content(~q[
<div>thing</div>
])
assert {:ok, []} = compile(document)
end

test "ignore undefinied assigns" do
document = document_with_content(~q[
<div><%= @thing %></div>
])

assert {:error, []} = compile(document)
end

test "returns error when there are unclosed tags" do
document = document_with_content(~q[
<div>thing
])
assert {:error, [%Result{} = result]} = compile(document)

assert result.message =~
"end of template reached without closing tag for <div>\n |\n1 | <div>thing\n | ^"

assert result.position == {1, 1}
assert result.severity == :error
assert result.source == "HEEx"
assert result.uri =~ "file:///file.heex"
end

test "returns error when HEEx syntax is invalid" do
document = document_with_content(~q[
<span id=@id}></span>
])

assert {:error, [%Result{} = result]} = compile(document)

assert result.message =~ "invalid attribute value after `=`. "
assert result.position == {1, 10}
assert result.severity == :error
assert result.source == "HEEx"
assert result.uri =~ "file:///file.heex"
end

test "handles EEx syntax error" do
document = document_with_content(~q[
<%= IO.
])
assert {:error, [%Result{} = result]} = compile(document)

assert result.message =~ "'%>'"
assert result.source == "EEx"
end
end
end
12 changes: 12 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
%{
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
"castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
"credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"},
"dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"},
"earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"},
Expand All @@ -12,9 +13,20 @@
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"patch": {:hex, :patch, "0.12.0", "2da8967d382bade20344a3e89d618bfba563b12d4ac93955468e830777f816b0", [:mix], [], "hexpm", "ffd0e9a7f2ad5054f37af84067ee88b1ad337308a1cb227e181e3967127b0235"},
"path_glob": {:hex, :path_glob, "0.2.0", "b9e34b5045cac5ecb76ef1aa55281a52bf603bf7009002085de40958064ca312", [:mix], [{:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "be2594cb4553169a1a189f95193d910115f64f15f0d689454bb4e8cfae2e7ebc"},
"phoenix": {:hex, :phoenix, "1.7.7", "4cc501d4d823015007ba3cdd9c41ecaaf2ffb619d6fb283199fa8ddba89191e0", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "8966e15c395e5e37591b6ed0bd2ae7f48e961f0f60ac4c733f9566b519453085"},
"phoenix_html": {:hex, :phoenix_html, "3.3.2", "d6ce982c6d8247d2fc0defe625255c721fb8d5f1942c5ac051f6177bffa5973f", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "44adaf8e667c1c20fb9d284b6b0fa8dc7946ce29e81ce621860aa7e96de9a11d"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.19.5", "6e730595e8e9b8c5da230a814e557768828fd8dfeeb90377d2d8dbb52d4ec00a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b2eaa0dd3cfb9bd7fb949b88217df9f25aed915e986a28ad5c8a0d054e7ca9d3"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_template": {:hex, :phoenix_template, "1.0.3", "32de561eefcefa951aead30a1f94f1b5f0379bc9e340bb5c667f65f1edfa4326", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "16f4b6588a4152f3cc057b9d0c0ba7e82ee23afa65543da535313ad8d25d8e2c"},
"plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"},
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
"sourceror": {:hex, :sourceror, "0.14.0", "b6b8552d0240400d66b6f107c1bab7ac1726e998efc797f178b7b517e928e314", [:mix], [], "hexpm", "809c71270ad48092d40bbe251a133e49ae229433ce103f762a2373b7a10a8d8b"},
"stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.4", "7af8408e7ed9d56578539594d1ee7d8461e2dd5c3f57b0f2a5352d610ddde757", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d2c238c79c52cbe223fcdae22ca0bb5007a735b9e933870e241fce66afb4f4ab"},
}
Loading